diff --git a/README.md b/README.md index 45418f9..1de30b3 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ This integration allows you to control Mobilus Cosmo GTW devices from Home Assis ## Prerequisites -- Home Assistant installation. -- Local access to the Mobilus Cosmo GTW IP address and valid login credentials. +- Home Assistant installation version `2024.4.0` or later (earlier versions are available in older releases - see `hacs.json` for details). +- Local access to the Mobilus Cosmo GTW IP address and valid login credentials. Internet access is not required and can be disabled on the Mobilus Cosmo GTW device. ## Installation @@ -31,25 +31,16 @@ cp -r custom_components/mobilus /var/lib/home_assistant/custom_components/ ## Configuration -Add the following to your `configuration.yaml` file: +Once installed, add the integration to your Home Assistant instance through UI (Settings -> Devices & Services -> Add Integration -> Mobilus COSMO GTW) and follow the UI configure setup. -```yaml -cover: - - platform: mobilus - host: MOBILUS_COSMO_GTW_IP - username: MOBILUS_COSMO_GTW_USERNAME - password: MOBILUS_COSMO_GTW_PASSWORD -``` +If needed the setup can be reconfigured through "Reconfigure" in the integration settings. -Example: +Example configuration: -```yaml -cover: - - platform: mobilus host: 192.168.2.1 username: admin password: mypassword -``` + ## Caveats diff --git a/custom_components/mobilus/__init__.py b/custom_components/mobilus/__init__.py index e69de29..c308afd 100644 --- a/custom_components/mobilus/__init__.py +++ b/custom_components/mobilus/__init__.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import json +import logging +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from homeassistant.config_entries import ConfigEntry + from homeassistant.core import HomeAssistant + +from mobilus_client.app import App as MobilusClientApp +from mobilus_client.config import Config as MobilusClientConfig + +from .const import DOMAIN, PLATFORMS, SUPPORTED_DEVICES +from .coordinator import MobilusCoordinator + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + hass.data.setdefault(DOMAIN, {}) + + client_config = MobilusClientConfig( + gateway_host=entry.data["host"], + user_login=entry.data["username"], + user_password=entry.data["password"], + ) + client = MobilusClientApp(client_config) + + # Retrieve devices list + response = await hass.async_add_executor_job( + lambda: json.loads(client.call([("devices_list", {})])), + ) + + if not response: + _LOGGER.warning("No devices found in response.") + return False + + devices = response[0].get("devices", []) + + if not devices: + _LOGGER.warning("No devices found in the devices list.") + return False + + # Currently non cover devices are not supported + supported_devices = [ + device for device in devices + if device["type"] in SUPPORTED_DEVICES + ] + + if not supported_devices: + _LOGGER.warning("No supported devices found in the devices list.") + return False + + coordinator = MobilusCoordinator(hass, client) + + hass.data[DOMAIN][entry.entry_id] = { + "client": client, + "coordinator": coordinator, + "devices": supported_devices, + } + + await coordinator.async_config_entry_first_refresh() + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/custom_components/mobilus/config_flow.py b/custom_components/mobilus/config_flow.py new file mode 100644 index 0000000..0aa9be1 --- /dev/null +++ b/custom_components/mobilus/config_flow.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from typing import Any + +import voluptuous as vol +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult + +from .const import DOMAIN + + +class MobilusConfigFlow(ConfigFlow, domain=DOMAIN): + VERSION = 1 + + async def async_step_user(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: + if user_input is not None: + return self.async_create_entry( + title=user_input["host"], + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=self._data_schema(), + ) + + async def async_step_reconfigure(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: + reconfigure_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"], + ) + + if reconfigure_entry is None: + return self.async_abort(reason="entry_not_found") + + if user_input is not None: + return self.async_update_reload_and_abort( + entry=reconfigure_entry, + data=user_input, + reason="reconfigure_successful", + ) + + return self.async_show_form( + step_id="reconfigure", + data_schema=self._data_schema(dict(reconfigure_entry.data)), + ) + + def _data_schema(self, defaults: dict[str, Any] | None = None) -> vol.Schema: + defaults = defaults or {} + + return vol.Schema({ + vol.Required("host", default=defaults.get("host", None)): str, + vol.Required("username", default=defaults.get("username", None)): str, + vol.Required("password", default=defaults.get("password", None)): str, + }) diff --git a/custom_components/mobilus/const.py b/custom_components/mobilus/const.py index a81ca0b..9398c20 100644 --- a/custom_components/mobilus/const.py +++ b/custom_components/mobilus/const.py @@ -1,7 +1,11 @@ +from homeassistant.const import Platform + from .device import MobilusDevice DOMAIN = "mobilus" +PLATFORMS = [Platform.COVER] + NOT_SUPPORTED_DEVICES = ( MobilusDevice.CGR, MobilusDevice.SWITCH, diff --git a/custom_components/mobilus/cover.py b/custom_components/mobilus/cover.py index 8a07959..7ab37ee 100644 --- a/custom_components/mobilus/cover.py +++ b/custom_components/mobilus/cover.py @@ -1,81 +1,34 @@ from __future__ import annotations import asyncio -import json import logging from typing import TYPE_CHECKING, Any -import voluptuous from homeassistant.components.cover import CoverDeviceClass, CoverEntity, CoverEntityFeature -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import config_validation from homeassistant.helpers.update_coordinator import CoordinatorEntity -from mobilus_client.app import App as MobilusClientApp -from mobilus_client.config import Config as MobilusClientConfig -from .const import DOMAIN, POSITION_SUPPORTED_DEVICES, SUPPORTED_DEVICES +from .const import DOMAIN, POSITION_SUPPORTED_DEVICES from .coordinator import MobilusCoordinator if TYPE_CHECKING: + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback - from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + from mobilus_client.app import App as MobilusClientApp _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = voluptuous.Schema( - { - voluptuous.Required(CONF_HOST): config_validation.string, - voluptuous.Required(CONF_USERNAME): config_validation.string, - voluptuous.Required(CONF_PASSWORD): config_validation.string, - }, - extra=voluptuous.ALLOW_EXTRA, -) - -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - _discovery_info: DiscoveryInfoType | None = None, ) -> None: - - client_config = MobilusClientConfig( - gateway_host=config[CONF_HOST], - user_login=config[CONF_USERNAME], - user_password=config[CONF_PASSWORD], - ) - client = MobilusClientApp(client_config) - - # Retrieve devices list - response = await hass.async_add_executor_job( - lambda: json.loads(client.call([("devices_list", {})])), - ) - - if not response: - _LOGGER.warning("No devices found in response. Exiting platform setup.") - return - - devices = response[0].get("devices", []) - - if not devices: - _LOGGER.warning("No devices found in the devices list.") - return - - # Currently non cover devices are not supported - supported_devices = [ - device for device in devices - if device["type"] in SUPPORTED_DEVICES - ] - - if not supported_devices: - _LOGGER.warning("No supported devices found in the devices list.") - return - - coordinator = MobilusCoordinator(hass, client) - await coordinator.async_config_entry_first_refresh() + client = hass.data[DOMAIN][entry.entry_id]["client"] + devices = hass.data[DOMAIN][entry.entry_id]["devices"] + coordinator = hass.data[DOMAIN][entry.entry_id]["coordinator"] async_add_entities( - [MobilusCover(device, client, coordinator) for device in supported_devices], + [MobilusCover(device, client, coordinator) for device in devices], ) class MobilusCover(CoordinatorEntity[MobilusCoordinator], CoverEntity): @@ -168,6 +121,8 @@ async def async_set_cover_position(self, **kwargs: Any) -> None: # noqa: ANN401 await self.coordinator.async_request_refresh() async def async_added_to_hass(self) -> None: - self.async_on_remove( - self.coordinator.async_add_listener(self.async_write_ha_state), - ) + # Add a listener to the coordinator to update the entity's state on data changes + coordinator_listener = self.coordinator.async_add_listener(self.async_write_ha_state) + + # Register the listener for cleanup when the entity is removed from Home Assistant + self.async_on_remove(coordinator_listener) diff --git a/custom_components/mobilus/manifest.json b/custom_components/mobilus/manifest.json index e7689c4..849fdc0 100644 --- a/custom_components/mobilus/manifest.json +++ b/custom_components/mobilus/manifest.json @@ -4,13 +4,14 @@ "codeowners": [ "@zpieslak" ], + "config_flow": true, "dependencies": [], "documentation": "https://github.com/zpieslak/mobilus-client-home-assistant", "integration_type": "hub", "iot_class": "local_polling", "issue_tracker": "https://github.com/zpieslak/mobilus-client-home-assistant/issues", "requirements": [ - "mobilus-client==0.1.5" + "mobilus-client>=0.1.5" ], "version": "0.1.0" } diff --git a/custom_components/mobilus/strings.json b/custom_components/mobilus/strings.json new file mode 100644 index 0000000..2fbe5db --- /dev/null +++ b/custom_components/mobilus/strings.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "user": { + "title": "Connect to Mobilus COSMO GTW", + "description": "Please enter your Mobilus COSMO GTW host and credentials.", + "data": { + "host": "IP Address / Host", + "username": "Username", + "password": "Password" + } + }, + "reconfigure": { + "title": "Reconfigure Mobilus COSMO GTW", + "description": "Please enter your Mobilus COSMO GTW host and credentials.", + "data": { + "host": "IP Address / Host", + "username": "Username", + "password": "Password" + } + } + }, + "error": { + "entry_not_found": "Configuration entry not found.", + "reconfigure_successful": "Reconfiguration has been saved. If the data is incorrect, please enter the correct data again." + } + } +} diff --git a/custom_components/mobilus/translations/en.json b/custom_components/mobilus/translations/en.json new file mode 100644 index 0000000..2fbe5db --- /dev/null +++ b/custom_components/mobilus/translations/en.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "user": { + "title": "Connect to Mobilus COSMO GTW", + "description": "Please enter your Mobilus COSMO GTW host and credentials.", + "data": { + "host": "IP Address / Host", + "username": "Username", + "password": "Password" + } + }, + "reconfigure": { + "title": "Reconfigure Mobilus COSMO GTW", + "description": "Please enter your Mobilus COSMO GTW host and credentials.", + "data": { + "host": "IP Address / Host", + "username": "Username", + "password": "Password" + } + } + }, + "error": { + "entry_not_found": "Configuration entry not found.", + "reconfigure_successful": "Reconfiguration has been saved. If the data is incorrect, please enter the correct data again." + } + } +} diff --git a/custom_components/mobilus/translations/pl.json b/custom_components/mobilus/translations/pl.json new file mode 100644 index 0000000..627a395 --- /dev/null +++ b/custom_components/mobilus/translations/pl.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "user": { + "title": "Podaj dane połączenia", + "description": "Proszę podać dane do połączenia z bramką Mobilus COSMO GTW.", + "data": { + "host": "Adres IP / Host", + "username": "Nazwa użytkownika", + "password": "Hasło" + } + }, + "reconfigure": { + "title": "Zmień dane połączenia", + "description": "Proszę podać dane do połączenia z bramką Mobilus COSMO GTW.", + "data": { + "host": "Adres IP / Host", + "username": "Nazwa użytkownika", + "password": "Hasło" + } + } + }, + "error": { + "entry_not_found": "Konfiguracja nie została znaleziona.", + "reconfigure_successful": "Ponowna konfiguracja została zapisana. W przypadku błędnych danych, proszę ponownie wprowadzić poprawne dane." + } + } +} diff --git a/hacs.json b/hacs.json index 8186d89..f4ba09c 100644 --- a/hacs.json +++ b/hacs.json @@ -1,4 +1,4 @@ { "name": "Mobilus Cosmo GTW", - "homeassistant": "2022.5.0" + "homeassistant": "2024.4.0" } diff --git a/pyproject.toml b/pyproject.toml index 7955aa9..8d91e78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "mobilus-client-home-assistant" dynamic = ["version"] dependencies = [ - "homeassistant>=2022.5.0", + "homeassistant>=2024.4.0", "mobilus-client==0.1.5", ] @@ -34,7 +34,7 @@ explicit_package_bases = true strict = true [[tool.mypy.overrides]] -module = ["homeassistant.*", "voluptuous.*"] +module = ["homeassistant.*", "pytest_homeassistant_custom_component.*", "voluptuous.*"] ignore_missing_imports = true [tool.ruff] @@ -48,4 +48,4 @@ select = ["ALL"] asyncio_mode = "auto" [tool.ruff.lint.extend-per-file-ignores] -"tests/**/*.py" = ["S101", "PLR2004"] +"tests/**/*.py" = ["PLR0913", "PLR2004", "S101"] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..d12eba4 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,34 @@ +from typing import Generator +from unittest.mock import AsyncMock, Mock, patch + +import pytest +from pytest_homeassistant_custom_component.common import MockConfigEntry + +from custom_components.mobilus.const import DOMAIN + + +@pytest.fixture +def mock_client() -> Generator[Mock, None, None]: + with patch("custom_components.mobilus.MobilusClientApp", autospec=True) as mock_client_class: + mock_instance = mock_client_class.return_value + yield mock_instance + +@pytest.fixture +def mock_coordinator() -> Generator[Mock, None, None]: + with patch("custom_components.mobilus.MobilusCoordinator") as mock_coordinator_class: + mock_instance = mock_coordinator_class.return_value + mock_instance.async_config_entry_first_refresh = AsyncMock() + mock_instance.async_request_refresh = AsyncMock() + mock_instance.async_add_listener = Mock() + yield mock_instance + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + return MockConfigEntry( + domain=DOMAIN, + data = { + "host": "test_host", + "username": "test_user", + "password": "test_pass", + }, + ) diff --git a/tests/test_cover.py b/tests/test_cover.py index fab8955..9e27f33 100644 --- a/tests/test_cover.py +++ b/tests/test_cover.py @@ -1,183 +1,60 @@ from __future__ import annotations -import json from typing import TYPE_CHECKING, Generator -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import Mock, patch import pytest from homeassistant.components.cover import CoverDeviceClass, CoverEntityFeature -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from custom_components.mobilus.cover import ( - MobilusCover, - async_setup_platform, -) +from custom_components.mobilus.const import DOMAIN +from custom_components.mobilus.cover import MobilusCover, async_setup_entry if TYPE_CHECKING: from homeassistant.core import HomeAssistant - - -@pytest.fixture -def mock_logger() -> Generator[Mock, None, None]: - with patch("custom_components.mobilus.cover._LOGGER", autospec=True) as mock_logger: - yield mock_logger - -@pytest.fixture -def mock_client() -> Generator[Mock, None, None]: - with patch("custom_components.mobilus.cover.MobilusClientApp", autospec=True) as mock_client_class: - mock_instance = mock_client_class.return_value - yield mock_instance - -@pytest.fixture -def mock_config() -> dict[str, str]: - return { - CONF_HOST: "test_host", - CONF_USERNAME: "test_user", - CONF_PASSWORD: "test_pass", - } + from pytest_homeassistant_custom_component.common import MockConfigEntry @pytest.fixture -def mock_coordinator() -> Generator[Mock, None, None]: - with patch("custom_components.mobilus.cover.MobilusCoordinator") as mock_coordinator_class: - mock_instance = mock_coordinator_class.return_value - mock_instance.async_config_entry_first_refresh = AsyncMock() - mock_instance.async_request_refresh = AsyncMock() - mock_instance.async_add_listener = Mock() - yield mock_instance +def mock_async_add_entities() -> Mock: + return Mock() @pytest.fixture def mock_asyncio_sleep() -> Generator[Mock, None, None]: with patch("asyncio.sleep", return_value=None) as mock_sleep: yield mock_sleep -async def test_async_setup_platform( - hass: HomeAssistant, mock_client: Mock, mock_config: dict[str, str], mock_coordinator: Mock) -> None: - mock_client.call.return_value = json.dumps( - [ - { - "devices": [ - { - "id": "0", - "name": "Device SENSO", - "type": 1, - }, - { - "id": "1", - "name": "Device COSMO", - "type": 2, - }, - { - "id": "2", - "name": "Device CMR", - "type": 3, - }, - { - "id": "3", - "name": "Device CGR", - "type": 4, - }, - { - "id": "4", - "name": "Device SWITCH", - "type": 5, - }, - { - "id": "5", - "name": "Device SWITCH_NP", - "type": 6, - }, - { - "id": "6", - "name": "Device COSMO_CZR", - "type": 7, - }, - { - "id": "7", - "name": "Device COSMO_MZR", - "type": 8, - }, - { - "id": "8", - "name": "Device SENSO_Z", - "type": 9, - }, - ]}, - ], - ) - - async_add_entities = Mock() - - await async_setup_platform(hass, mock_config, async_add_entities) - - assert async_add_entities.call_count == 1 - assert mock_coordinator.async_config_entry_first_refresh.call_count == 1 - - entities = async_add_entities.call_args[0][0] - assert len(entities) == 6 - assert isinstance(entities[0], MobilusCover) - assert entities[0].device["id"] == "0" - assert entities[1].device["id"] == "1" - assert entities[2].device["id"] == "2" - assert entities[3].device["id"] == "6" - assert entities[4].device["id"] == "7" - assert entities[5].device["id"] == "8" - +async def test_async_setup_entry( + hass: HomeAssistant, mock_client: Mock, mock_coordinator: Mock, + mock_config_entry: MockConfigEntry, mock_async_add_entities: Mock) -> None: -async def test_async_setup_platform_no_devices( - hass: HomeAssistant, mock_client: Mock, mock_config: dict[str, str], - mock_coordinator: Mock, mock_logger: Mock) -> None: - mock_client.call.return_value = json.dumps([]) - - async_add_entities = Mock() - await async_setup_platform(hass, mock_config, async_add_entities) - - mock_logger.warning.assert_called_once_with("No devices found in response. Exiting platform setup.") - assert async_add_entities.call_count == 0 - assert mock_coordinator.async_config_entry_first_refresh.call_count == 0 - - -async def test_async_setup_platform_no_device_in_response( - hass: HomeAssistant, mock_client: Mock, mock_config: dict[str, str], - mock_coordinator: Mock, mock_logger: Mock) -> None: - mock_client.call.return_value = json.dumps( - [ - { - "devices": [], - }, - ], - ) + device_senso = { + "id": "0", + "name": "Device SENSO", + "type": 1, + } + device_cosmo = { + "id": "1", + "name": "Device COSMO", + "type": 2, + } - async_add_entities = Mock() - await async_setup_platform(hass, mock_config, async_add_entities) - - mock_logger.warning.assert_called_once_with("No devices found in the devices list.") - assert async_add_entities.call_count == 0 - assert mock_coordinator.async_config_entry_first_refresh.call_count == 0 - -async def test_async_setup_platform_no_supported_devices( - hass: HomeAssistant, mock_client: Mock, mock_config: dict[str, str], - mock_coordinator: Mock, mock_logger: Mock) -> None: - - mock_client.call.return_value = json.dumps( - [ - { - "devices": [ - { - "id": "0", - "name": "Device SWITCH", - "type": 5, - }, - ]}, + hass.data[DOMAIN] = {} + hass.data[DOMAIN][mock_config_entry.entry_id] = { + "client": mock_client, + "coordinator": mock_coordinator, + "devices": [ + device_senso, + device_cosmo, ], - ) - - async_add_entities = Mock() - await async_setup_platform(hass, mock_config, async_add_entities) + } - mock_logger.warning.assert_called_once_with("No supported devices found in the devices list.") - assert async_add_entities.call_count == 0 - assert mock_coordinator.async_config_entry_first_refresh.call_count == 0 + await async_setup_entry(hass, mock_config_entry, mock_async_add_entities) + assert mock_async_add_entities.call_with( + [ + MobilusCover(device_senso, mock_client, mock_coordinator), + MobilusCover(device_cosmo, mock_client, mock_coordinator), + ], + ) def test_cover_init(mock_client: Mock, mock_coordinator: Mock) -> None: device = { @@ -449,7 +326,7 @@ async def test_cover_async_added_to_hass(hass: HomeAssistant, mock_client: Mock, cover = MobilusCover(device, mock_client, mock_coordinator) cover.hass = hass - with patch.object(cover, "async_on_remove", new=AsyncMock()) as mock_async_on_remove: + with patch.object(cover, "async_on_remove", new=Mock()) as mock_async_on_remove: await cover.async_added_to_hass() mock_coordinator.async_add_listener.assert_called_once_with(cover.async_write_ha_state) diff --git a/tests/test_init.py b/tests/test_init.py new file mode 100644 index 0000000..61003f4 --- /dev/null +++ b/tests/test_init.py @@ -0,0 +1,222 @@ +from __future__ import annotations + +import json +from typing import TYPE_CHECKING, Generator +from unittest.mock import AsyncMock, Mock, patch + +import pytest + +from custom_components.mobilus import async_setup_entry, async_unload_entry +from custom_components.mobilus.const import DOMAIN, PLATFORMS + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + from pytest_homeassistant_custom_component.common import MockConfigEntry + +@pytest.fixture +def mock_forward_entry_setups(hass: HomeAssistant) -> Generator[AsyncMock, None, None]: + with patch.object(hass.config_entries, "async_forward_entry_setups", new=AsyncMock()) as mock_forward_entry_setups: + yield mock_forward_entry_setups + +@pytest.fixture +def mock_unload_platforms(hass: HomeAssistant) -> Generator[AsyncMock, None, None]: + with patch.object(hass.config_entries, "async_unload_platforms", new=AsyncMock()) as mock_unload_platforms: + yield mock_unload_platforms + + +@pytest.fixture +def mock_logger() -> Generator[Mock, None, None]: + with patch("custom_components.mobilus._LOGGER", autospec=True) as mock_logger: + yield mock_logger + +async def test_async_setup_entry( + hass: HomeAssistant, mock_client: Mock, mock_config_entry: MockConfigEntry, + mock_coordinator: Mock, mock_forward_entry_setups: AsyncMock) -> None: + + mock_client.call.return_value = json.dumps( + [ + { + "devices": [ + { + "id": "0", + "name": "Device SENSO", + "type": 1, + }, + { + "id": "1", + "name": "Device COSMO", + "type": 2, + }, + { + "id": "2", + "name": "Device CMR", + "type": 3, + }, + { + "id": "3", + "name": "Device CGR", + "type": 4, + }, + { + "id": "4", + "name": "Device SWITCH", + "type": 5, + }, + { + "id": "5", + "name": "Device SWITCH_NP", + "type": 6, + }, + { + "id": "6", + "name": "Device COSMO_CZR", + "type": 7, + }, + { + "id": "7", + "name": "Device COSMO_MZR", + "type": 8, + }, + { + "id": "8", + "name": "Device SENSO_Z", + "type": 9, + }, + ]}, + ], + ) + + result = await async_setup_entry(hass, mock_config_entry) + + assert result + assert mock_coordinator.async_config_entry_first_refresh.call_count == 1 + mock_forward_entry_setups.assert_called_once_with(mock_config_entry, PLATFORMS) + assert(hass.data[DOMAIN][mock_config_entry.entry_id]) == { + "client": mock_client, + "coordinator": mock_coordinator, + "devices": [ + { + "id": "0", + "name": "Device SENSO", + "type": 1, + }, + { + "id": "1", + "name": "Device COSMO", + "type": 2, + }, + { + "id": "2", + "name": "Device CMR", + "type": 3, + }, + { + "id": "6", + "name": "Device COSMO_CZR", + "type": 7, + }, + { + "id": "7", "name": "Device COSMO_MZR", + "type": 8, + }, + { + "id": "8", + "name": "Device SENSO_Z", + "type": 9, + }, + ], + } + +async def test_async_setup_entry_no_devices( + hass: HomeAssistant, mock_client: Mock, mock_config_entry: MockConfigEntry, + mock_coordinator: Mock, mock_forward_entry_setups: AsyncMock, mock_logger: Mock) -> None: + + mock_client.call.return_value = json.dumps([]) + + result = await async_setup_entry(hass, mock_config_entry) + + assert not result + mock_logger.warning.assert_called_once_with("No devices found in response.") + assert(hass.data[DOMAIN]) == {} + assert mock_coordinator.async_config_entry_first_refresh.call_count == 0 + assert mock_forward_entry_setups.call_count == 0 + + +async def test_async_setup_entry_no_device_in_response( + hass: HomeAssistant, mock_client: Mock, mock_config_entry: MockConfigEntry, + mock_coordinator: Mock, mock_forward_entry_setups: AsyncMock, mock_logger: Mock) -> None: + + mock_client.call.return_value = json.dumps( + [ + { + "devices": [], + }, + ], + ) + + await async_setup_entry(hass, mock_config_entry) + + mock_logger.warning.assert_called_once_with("No devices found in the devices list.") + assert(hass.data[DOMAIN]) == {} + assert mock_coordinator.async_config_entry_first_refresh.call_count == 0 + assert mock_forward_entry_setups.call_count == 0 + +async def test_async_setup_entry_no_supported_devices( + hass: HomeAssistant, mock_client: Mock, mock_config_entry: MockConfigEntry, + mock_coordinator: Mock, mock_forward_entry_setups: AsyncMock, mock_logger: Mock) -> None: + + mock_client.call.return_value = json.dumps( + [ + { + "devices": [ + { + "id": "0", + "name": "Device SWITCH", + "type": 5, + }, + ]}, + ], + ) + + result = await async_setup_entry(hass, mock_config_entry) + + assert not result + mock_logger.warning.assert_called_once_with("No supported devices found in the devices list.") + assert mock_coordinator.async_config_entry_first_refresh.call_count == 0 + assert mock_forward_entry_setups.call_count == 0 + +async def test_async_setup_unload_entry( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_unload_platforms: AsyncMock) -> None: + + mock_unload_platforms.return_value = True + hass_domain = { + "client": Mock(), + "coordinator": Mock(), + "devices": [], + } + hass.data[DOMAIN] = {} + hass.data[DOMAIN][mock_config_entry.entry_id] = hass_domain + + result = await async_unload_entry(hass, mock_config_entry) + + assert result + assert mock_unload_platforms.call_count == 1 + assert not hass.data[DOMAIN] + +async def test_async_setup_unload_entry_false( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_unload_platforms: AsyncMock) -> None: + + mock_unload_platforms.return_value = False + hass_domain = { + "client": Mock(), + "coordinator": Mock(), + "devices": [], + } + hass.data[DOMAIN] = {} + hass.data[DOMAIN][mock_config_entry.entry_id] = hass_domain + + result = await async_unload_entry(hass, mock_config_entry) + + assert not result + assert mock_unload_platforms.call_count == 1 + assert hass.data[DOMAIN][mock_config_entry.entry_id] == hass_domain