diff --git a/homeassistant/components/kostal_plenticore/config_flow.py b/homeassistant/components/kostal_plenticore/config_flow.py index cbbaeefd85d..ba8e762763d 100644 --- a/homeassistant/components/kostal_plenticore/config_flow.py +++ b/homeassistant/components/kostal_plenticore/config_flow.py @@ -12,6 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN +from .helper import get_hostname_id _LOGGER = logging.getLogger(__name__) @@ -32,9 +33,10 @@ async def test_connection(hass: HomeAssistant, data) -> str: session = async_get_clientsession(hass) async with ApiClient(session, data["host"]) as client: await client.login(data["password"]) - values = await client.get_setting_values("scb:network", "Hostname") + hostname_id = await get_hostname_id(client) + values = await client.get_setting_values("scb:network", hostname_id) - return values["scb:network"]["Hostname"] + return values["scb:network"][hostname_id] class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py index a91fb24aad7..35ec7bb9456 100644 --- a/homeassistant/components/kostal_plenticore/helper.py +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -23,6 +23,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) _DataT = TypeVar("_DataT") +_KNOWN_HOSTNAME_IDS = ("Network:Hostname", "Hostname") class Plenticore: @@ -69,6 +70,7 @@ class Plenticore: ) # get some device meta data + hostname_id = await get_hostname_id(self._client) settings = await self._client.get_setting_values( { "devices:local": [ @@ -78,7 +80,7 @@ class Plenticore: "Properties:VersionIOC", "Properties:VersionMC", ], - "scb:network": ["Hostname"], + "scb:network": [hostname_id], } ) @@ -91,7 +93,7 @@ class Plenticore: identifiers={(DOMAIN, device_local["Properties:SerialNo"])}, manufacturer="Kostal", model=f"{prod1} {prod2}", - name=settings["scb:network"]["Hostname"], + name=settings["scb:network"][hostname_id], sw_version=f'IOC: {device_local["Properties:VersionIOC"]}' + f' MC: {device_local["Properties:VersionMC"]}', ) @@ -403,3 +405,12 @@ class PlenticoreDataFormatter: return state return PlenticoreDataFormatter.EM_STATES.get(value) + + +async def get_hostname_id(client: ApiClient) -> str: + """Check for known existing hostname ids.""" + all_settings = await client.get_settings() + for entry in all_settings["scb:network"]: + if entry.id in _KNOWN_HOSTNAME_IDS: + return entry.id + raise ApiException("Hostname identifier not found in KNOWN_HOSTNAME_IDS") diff --git a/tests/components/kostal_plenticore/test_config_flow.py b/tests/components/kostal_plenticore/test_config_flow.py index 3c64a48c218..41facfe9c26 100644 --- a/tests/components/kostal_plenticore/test_config_flow.py +++ b/tests/components/kostal_plenticore/test_config_flow.py @@ -1,8 +1,10 @@ """Test the Kostal Plenticore Solar Inverter config flow.""" import asyncio +from collections.abc import Generator from unittest.mock import ANY, AsyncMock, MagicMock, patch -from pykoplenti import AuthenticationException +from pykoplenti import ApiClient, AuthenticationException, SettingsData +import pytest from homeassistant import config_entries from homeassistant.components.kostal_plenticore.const import DOMAIN @@ -11,8 +13,33 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -async def test_formx(hass: HomeAssistant) -> None: - """Test we get the form.""" +@pytest.fixture +def mock_apiclient() -> ApiClient: + """Return a mocked ApiClient instance.""" + apiclient = MagicMock(spec=ApiClient) + apiclient.__aenter__.return_value = apiclient + apiclient.__aexit__ = AsyncMock() + + return apiclient + + +@pytest.fixture +def mock_apiclient_class(mock_apiclient) -> Generator[type[ApiClient], None, None]: + """Return a mocked ApiClient class.""" + with patch( + "homeassistant.components.kostal_plenticore.config_flow.ApiClient", + autospec=True, + ) as mock_api_class: + mock_api_class.return_value = mock_apiclient + yield mock_api_class + + +async def test_form_g1( + hass: HomeAssistant, + mock_apiclient_class: type[ApiClient], + mock_apiclient: ApiClient, +) -> None: + """Test the config flow for G1 models.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -21,25 +48,19 @@ async def test_formx(hass: HomeAssistant) -> None: assert result["errors"] == {} with patch( - "homeassistant.components.kostal_plenticore.config_flow.ApiClient" - ) as mock_api_class, patch( "homeassistant.components.kostal_plenticore.async_setup_entry", return_value=True, ) as mock_setup_entry: # mock of the context manager instance - mock_api_ctx = MagicMock() - mock_api_ctx.login = AsyncMock() - mock_api_ctx.get_setting_values = AsyncMock( + mock_apiclient.login = AsyncMock() + mock_apiclient.get_settings = AsyncMock( + return_value={"scb:network": [SettingsData({"id": "Hostname"})]} + ) + mock_apiclient.get_setting_values = AsyncMock( + # G1 model has the entry id "Hostname" return_value={"scb:network": {"Hostname": "scb"}} ) - # mock of the return instance of ApiClient - mock_api = MagicMock() - mock_api.__aenter__.return_value = mock_api_ctx - mock_api.__aexit__ = AsyncMock() - - mock_api_class.return_value = mock_api - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -49,11 +70,68 @@ async def test_formx(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - mock_api_class.assert_called_once_with(ANY, "1.1.1.1") - mock_api.__aenter__.assert_called_once() - mock_api.__aexit__.assert_called_once() - mock_api_ctx.login.assert_called_once_with("test-password") - mock_api_ctx.get_setting_values.assert_called_once() + mock_apiclient_class.assert_called_once_with(ANY, "1.1.1.1") + mock_apiclient.__aenter__.assert_called_once() + mock_apiclient.__aexit__.assert_called_once() + mock_apiclient.login.assert_called_once_with("test-password") + mock_apiclient.get_settings.assert_called_once() + mock_apiclient.get_setting_values.assert_called_once_with( + "scb:network", "Hostname" + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "scb" + assert result2["data"] == { + "host": "1.1.1.1", + "password": "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_g2( + hass: HomeAssistant, + mock_apiclient_class: type[ApiClient], + mock_apiclient: ApiClient, +) -> None: + """Test the config flow for G2 models.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.kostal_plenticore.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + # mock of the context manager instance + mock_apiclient.login = AsyncMock() + mock_apiclient.get_settings = AsyncMock( + return_value={"scb:network": [SettingsData({"id": "Network:Hostname"})]} + ) + mock_apiclient.get_setting_values = AsyncMock( + # G1 model has the entry id "Hostname" + return_value={"scb:network": {"Network:Hostname": "scb"}} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + mock_apiclient_class.assert_called_once_with(ANY, "1.1.1.1") + mock_apiclient.__aenter__.assert_called_once() + mock_apiclient.__aexit__.assert_called_once() + mock_apiclient.login.assert_called_once_with("test-password") + mock_apiclient.get_settings.assert_called_once() + mock_apiclient.get_setting_values.assert_called_once_with( + "scb:network", "Network:Hostname" + ) assert result2["type"] == "create_entry" assert result2["title"] == "scb" diff --git a/tests/components/kostal_plenticore/test_helper.py b/tests/components/kostal_plenticore/test_helper.py new file mode 100644 index 00000000000..cc522c96974 --- /dev/null +++ b/tests/components/kostal_plenticore/test_helper.py @@ -0,0 +1,107 @@ +"""Test Kostal Plenticore helper.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +from pykoplenti import ApiClient, SettingsData +import pytest + +from homeassistant.components.kostal_plenticore.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_apiclient() -> Generator[ApiClient, None, None]: + """Return a mocked ApiClient class.""" + with patch( + "homeassistant.components.kostal_plenticore.helper.ApiClient", + autospec=True, + ) as mock_api_class: + apiclient = MagicMock(spec=ApiClient) + apiclient.__aenter__.return_value = apiclient + apiclient.__aexit__ = AsyncMock() + mock_api_class.return_value = apiclient + yield apiclient + + +async def test_plenticore_async_setup_g1( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_apiclient: ApiClient, +) -> None: + """Tests the async_setup() method of the Plenticore class for G1 models.""" + mock_apiclient.get_settings = AsyncMock( + return_value={"scb:network": [SettingsData({"id": "Hostname"})]} + ) + mock_apiclient.get_setting_values = AsyncMock( + # G1 model has the entry id "Hostname" + return_value={ + "devices:local": { + "Properties:SerialNo": "12345", + "Branding:ProductName1": "PLENTICORE", + "Branding:ProductName2": "plus 10", + "Properties:VersionIOC": "01.45", + "Properties:VersionMC": "01.46", + }, + "scb:network": {"Hostname": "scb"}, + } + ) + + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + plenticore = hass.data[DOMAIN][mock_config_entry.entry_id] + + assert plenticore.device_info == DeviceInfo( + configuration_url="http://192.168.1.2", + identifiers={(DOMAIN, "12345")}, + manufacturer="Kostal", + model="PLENTICORE plus 10", + name="scb", + sw_version="IOC: 01.45 MC: 01.46", + ) + + +async def test_plenticore_async_setup_g2( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_apiclient: ApiClient, +) -> None: + """Tests the async_setup() method of the Plenticore class for G2 models.""" + mock_apiclient.get_settings = AsyncMock( + return_value={"scb:network": [SettingsData({"id": "Network:Hostname"})]} + ) + mock_apiclient.get_setting_values = AsyncMock( + # G1 model has the entry id "Hostname" + return_value={ + "devices:local": { + "Properties:SerialNo": "12345", + "Branding:ProductName1": "PLENTICORE", + "Branding:ProductName2": "plus 10", + "Properties:VersionIOC": "01.45", + "Properties:VersionMC": "01.46", + }, + "scb:network": {"Network:Hostname": "scb"}, + } + ) + + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + plenticore = hass.data[DOMAIN][mock_config_entry.entry_id] + + assert plenticore.device_info == DeviceInfo( + configuration_url="http://192.168.1.2", + identifiers={(DOMAIN, "12345")}, + manufacturer="Kostal", + model="PLENTICORE plus 10", + name="scb", + sw_version="IOC: 01.45 MC: 01.46", + ) diff --git a/tests/components/kostal_plenticore/test_number.py b/tests/components/kostal_plenticore/test_number.py index beabd8fe669..009184a699c 100644 --- a/tests/components/kostal_plenticore/test_number.py +++ b/tests/components/kostal_plenticore/test_number.py @@ -62,7 +62,20 @@ def mock_get_setting_values(mock_plenticore_client: ApiClient) -> list: "id": "Battery:MinHomeComsumption", } ), - ] + ], + "scb:network": [ + SettingsData( + { + "min": "1", + "default": None, + "access": "readwrite", + "unit": None, + "id": "Hostname", + "type": "string", + "max": "63", + } + ) + ], } # this values are always retrieved by the integration on startup @@ -112,7 +125,22 @@ async def test_setup_no_entries( ) -> None: """Test that no entries are setup if Plenticore does not provide data.""" - mock_plenticore_client.get_settings.return_value = [] + # remove all settings except hostname which is used during setup + mock_plenticore_client.get_settings.return_value = { + "scb:network": [ + SettingsData( + { + "min": "1", + "default": None, + "access": "readwrite", + "unit": None, + "id": "Hostname", + "type": "string", + "max": "63", + } + ) + ], + } mock_config_entry.add_to_hass(hass)