Files
core/tests/components/growatt_server/test_init.py
2025-12-29 15:54:14 +01:00

396 lines
12 KiB
Python

"""Tests for the Growatt Server integration."""
from datetime import timedelta
import json
from freezegun.api import FrozenDateTimeFactory
import growattServer
import pytest
import requests
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.growatt_server.const import (
AUTH_API_TOKEN,
AUTH_PASSWORD,
CONF_AUTH_TYPE,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_URL, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import setup_integration
from tests.common import MockConfigEntry, async_fire_time_changed
@pytest.mark.usefixtures("init_integration")
async def test_load_unload_config_entry(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test loading and unloading the integration."""
assert mock_config_entry.state is ConfigEntryState.LOADED
await hass.config_entries.async_unload(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
@pytest.mark.usefixtures("init_integration")
async def test_device_info(
snapshot: SnapshotAssertion,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test device registry integration."""
device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "MIN123456")})
assert device_entry is not None
assert device_entry == snapshot
@pytest.mark.parametrize(
("exception", "expected_state"),
[
(growattServer.GrowattV1ApiError("API Error"), ConfigEntryState.SETUP_ERROR),
(
json.decoder.JSONDecodeError("Invalid JSON", "", 0),
ConfigEntryState.SETUP_ERROR,
),
],
)
async def test_setup_error_on_api_failure(
hass: HomeAssistant,
mock_growatt_v1_api,
mock_config_entry: MockConfigEntry,
exception: Exception,
expected_state: ConfigEntryState,
) -> None:
"""Test setup error on API failures during device list."""
mock_growatt_v1_api.device_list.side_effect = exception
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is expected_state
@pytest.mark.usefixtures("init_integration")
async def test_coordinator_update_failed(
hass: HomeAssistant,
mock_growatt_v1_api,
mock_config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test coordinator handles update failures gracefully."""
# Integration should be loaded
assert mock_config_entry.state is ConfigEntryState.LOADED
# Cause coordinator update to fail
mock_growatt_v1_api.min_detail.side_effect = growattServer.GrowattV1ApiError(
"Connection timeout"
)
# Trigger coordinator refresh
freezer.tick(timedelta(minutes=5))
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# Integration should remain loaded despite coordinator error
assert mock_config_entry.state is ConfigEntryState.LOADED
async def test_classic_api_setup(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_growatt_classic_api,
mock_config_entry_classic: MockConfigEntry,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test integration setup with Classic API (password auth)."""
# Classic API doesn't support MIN devices - use TLX device instead
mock_growatt_classic_api.device_list.return_value = [
{"deviceSn": "TLX123456", "deviceType": "tlx"}
]
await setup_integration(hass, mock_config_entry_classic)
assert mock_config_entry_classic.state is ConfigEntryState.LOADED
# Verify Classic API login was called
mock_growatt_classic_api.login.assert_called()
# Verify device was created
device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "TLX123456")})
assert device_entry is not None
assert device_entry == snapshot
async def test_migrate_legacy_api_token_config(
hass: HomeAssistant,
mock_growatt_v1_api,
) -> None:
"""Test migration of legacy config entry with API token but no auth_type."""
# Create a legacy config entry without CONF_AUTH_TYPE
legacy_config = {
CONF_TOKEN: "test_token_123",
CONF_URL: "https://openapi.growatt.com/",
"plant_id": "plant_123",
}
mock_config_entry = MockConfigEntry(
domain=DOMAIN,
data=legacy_config,
unique_id="plant_123",
)
await setup_integration(hass, mock_config_entry)
# Verify migration occurred and auth_type was added
assert mock_config_entry.data[CONF_AUTH_TYPE] == AUTH_API_TOKEN
assert mock_config_entry.state is ConfigEntryState.LOADED
async def test_migrate_legacy_password_config(
hass: HomeAssistant,
mock_growatt_classic_api,
) -> None:
"""Test migration of legacy config entry with password auth but no auth_type."""
# Create a legacy config entry without CONF_AUTH_TYPE
legacy_config = {
CONF_USERNAME: "test_user",
CONF_PASSWORD: "test_password",
CONF_URL: "https://server.growatt.com/",
"plant_id": "plant_456",
}
mock_config_entry = MockConfigEntry(
domain=DOMAIN,
data=legacy_config,
unique_id="plant_456",
)
# Classic API doesn't support MIN devices - use TLX device instead
mock_growatt_classic_api.device_list.return_value = [
{"deviceSn": "TLX123456", "deviceType": "tlx"}
]
await setup_integration(hass, mock_config_entry)
# Verify migration occurred and auth_type was added
assert mock_config_entry.data[CONF_AUTH_TYPE] == AUTH_PASSWORD
assert mock_config_entry.state is ConfigEntryState.LOADED
async def test_migrate_legacy_config_no_auth_fields(
hass: HomeAssistant,
) -> None:
"""Test that config entry with no recognizable auth fields raises error."""
# Create a config entry without any auth fields
invalid_config = {
CONF_URL: "https://openapi.growatt.com/",
"plant_id": "plant_789",
}
mock_config_entry = MockConfigEntry(
domain=DOMAIN,
data=invalid_config,
unique_id="plant_789",
)
await setup_integration(hass, mock_config_entry)
# The ConfigEntryError is caught by the config entry system
# and the entry state is set to SETUP_ERROR
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
@pytest.mark.parametrize(
"exception",
[
requests.exceptions.RequestException("Connection error"),
json.decoder.JSONDecodeError("Invalid JSON", "", 0),
],
ids=["network_error", "json_error"],
)
async def test_classic_api_login_exceptions(
hass: HomeAssistant,
mock_growatt_classic_api,
mock_config_entry_classic: MockConfigEntry,
exception: Exception,
) -> None:
"""Test Classic API setup with login exceptions."""
mock_growatt_classic_api.login.side_effect = exception
await setup_integration(hass, mock_config_entry_classic)
assert mock_config_entry_classic.state is ConfigEntryState.SETUP_ERROR
@pytest.mark.parametrize(
"login_response",
[
{"success": False, "msg": "502"},
{"success": False, "msg": "Server maintenance"},
],
ids=["invalid_auth", "other_login_error"],
)
async def test_classic_api_login_failures(
hass: HomeAssistant,
mock_growatt_classic_api,
mock_config_entry_classic: MockConfigEntry,
login_response: dict,
) -> None:
"""Test Classic API setup with login failures."""
mock_growatt_classic_api.login.return_value = login_response
await setup_integration(hass, mock_config_entry_classic)
assert mock_config_entry_classic.state is ConfigEntryState.SETUP_ERROR
@pytest.mark.parametrize(
"exception",
[
requests.exceptions.RequestException("Connection error"),
json.decoder.JSONDecodeError("Invalid JSON", "", 0),
],
ids=["network_error", "json_error"],
)
async def test_classic_api_plant_list_exceptions(
hass: HomeAssistant,
mock_growatt_classic_api,
mock_config_entry_classic_default_plant: MockConfigEntry,
exception: Exception,
) -> None:
"""Test Classic API setup with plant list exceptions (default plant_id path)."""
# Login succeeds
mock_growatt_classic_api.login.return_value = {
"success": True,
"user": {"id": 123456},
}
# But plant_list raises exception
mock_growatt_classic_api.plant_list.side_effect = exception
await setup_integration(hass, mock_config_entry_classic_default_plant)
assert mock_config_entry_classic_default_plant.state is ConfigEntryState.SETUP_ERROR
async def test_classic_api_plant_list_no_plants(
hass: HomeAssistant,
mock_growatt_classic_api,
mock_config_entry_classic_default_plant: MockConfigEntry,
) -> None:
"""Test Classic API setup when plant list returns no plants."""
# Login succeeds
mock_growatt_classic_api.login.return_value = {
"success": True,
"user": {"id": 123456},
}
# But plant_list returns empty list
mock_growatt_classic_api.plant_list.return_value = {"data": []}
await setup_integration(hass, mock_config_entry_classic_default_plant)
assert mock_config_entry_classic_default_plant.state is ConfigEntryState.SETUP_ERROR
@pytest.mark.parametrize(
"exception",
[
requests.exceptions.RequestException("Connection error"),
json.decoder.JSONDecodeError("Invalid JSON", "", 0),
],
ids=["network_error", "json_error"],
)
async def test_classic_api_device_list_errors(
hass: HomeAssistant,
mock_growatt_classic_api,
mock_config_entry_classic: MockConfigEntry,
exception: Exception,
) -> None:
"""Test Classic API setup with device list errors."""
mock_growatt_classic_api.device_list.side_effect = exception
await setup_integration(hass, mock_config_entry_classic)
assert mock_config_entry_classic.state is ConfigEntryState.SETUP_ERROR
async def test_unknown_api_version(
hass: HomeAssistant,
) -> None:
"""Test setup with unknown API version."""
# Create a config entry with invalid auth type
config = {
CONF_URL: "https://openapi.growatt.com/",
"plant_id": "plant_123",
CONF_AUTH_TYPE: "unknown_auth", # Invalid auth type
}
mock_config_entry = MockConfigEntry(
domain=DOMAIN,
data=config,
unique_id="plant_123",
)
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
async def test_classic_api_auto_select_plant(
hass: HomeAssistant,
mock_growatt_classic_api,
mock_config_entry_classic_default_plant: MockConfigEntry,
) -> None:
"""Test Classic API setup with default plant ID (auto-selects first plant)."""
# Login succeeds and plant_list returns a plant
mock_growatt_classic_api.login.return_value = {
"success": True,
"user": {"id": 123456},
}
mock_growatt_classic_api.plant_list.return_value = {
"data": [{"plantId": "AUTO_PLANT_123", "plantName": "Auto Plant"}]
}
mock_growatt_classic_api.device_list.return_value = [
{"deviceSn": "TLX999999", "deviceType": "tlx"}
]
await setup_integration(hass, mock_config_entry_classic_default_plant)
# Should be loaded successfully with auto-selected plant
assert mock_config_entry_classic_default_plant.state is ConfigEntryState.LOADED
async def test_v1_api_unsupported_device_type(
hass: HomeAssistant,
mock_growatt_v1_api,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test V1 API logs warning for unsupported device types (non-MIN)."""
config = {
CONF_TOKEN: "test_token_123",
CONF_URL: "https://openapi.growatt.com/",
"plant_id": "plant_123",
CONF_AUTH_TYPE: AUTH_API_TOKEN,
}
mock_config_entry = MockConfigEntry(
domain=DOMAIN,
data=config,
unique_id="plant_123",
)
# Return mix of MIN (type 7) and other device types
mock_growatt_v1_api.device_list.return_value = {
"devices": [
{"device_sn": "MIN123456", "type": 7}, # Supported
{"device_sn": "TLX789012", "type": 5}, # Unsupported
]
}
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.LOADED
# Verify warning was logged for unsupported device
assert "Device TLX789012 with type 5 not supported in Open API V1" in caplog.text