From 95ab914fed3a6613849a4e2525cc2f4b2008ff33 Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Tue, 25 Jul 2023 23:25:01 +0200 Subject: [PATCH] Revert "split up session and rest" --- .gitignore | 3 +- .../husqvarna_automower/__init__.py | 45 +--- .../husqvarna_automower/config_flow.py | 4 +- .../husqvarna_automower/entity.py | 5 +- .../husqvarna_automower/manifest.json | 11 +- .../husqvarna_automower/tests/const.py | 37 +-- .../tests/test_application_credentials.py | 52 +++- .../tests/test_binary_sensor.py | 81 ++++-- .../tests/test_calendar.py | 100 +++++--- .../husqvarna_automower/tests/test_common.py | 94 ------- .../tests/test_config_flow.py | 196 ++++++++++----- .../tests/test_device_tracker.py | 69 +++-- .../tests/test_diagnostics.py | 85 ++++--- .../husqvarna_automower/tests/test_entity.py | 52 +++- .../husqvarna_automower/tests/test_image.py | 217 ++++++++-------- .../husqvarna_automower/tests/test_init.py | 235 +++++++++++------- .../husqvarna_automower/tests/test_number.py | 55 +++- .../husqvarna_automower/tests/test_select.py | 55 +++- .../husqvarna_automower/tests/test_sensor.py | 112 ++++++--- .../husqvarna_automower/tests/test_vacuum.py | 112 ++++----- 20 files changed, 950 insertions(+), 670 deletions(-) delete mode 100644 custom_components/husqvarna_automower/tests/test_common.py diff --git a/.gitignore b/.gitignore index 87eabe8..bf21ff8 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,4 @@ deps/ custom_components/husqvarna_automower/tests/output/*.png *.lock coverage.xml -.gitmodules -core/ \ No newline at end of file +.gitmodules \ No newline at end of file diff --git a/custom_components/husqvarna_automower/__init__.py b/custom_components/husqvarna_automower/__init__.py index bfb0215..a4dd582 100644 --- a/custom_components/husqvarna_automower/__init__.py +++ b/custom_components/husqvarna_automower/__init__.py @@ -2,7 +2,6 @@ import logging import os from asyncio.exceptions import TimeoutError as AsyncioTimeoutError -from datetime import timedelta import aioautomower from homeassistant.components.application_credentials import DATA_STORAGE @@ -41,15 +40,14 @@ def __init__(self, hass: HomeAssistant, *, entry: ConfigEntry) -> None: hass, _LOGGER, name=DOMAIN, - update_interval=timedelta(seconds=300), ) - self.api_key = None + api_key = None ap_storage = hass.data.get("application_credentials")[DATA_STORAGE] ap_storage_data = ap_storage.__dict__["data"] for k in ap_storage_data: - self.api_key = ap_storage_data[k]["client_id"] - self.access_token = entry.data.get(CONF_TOKEN) - if not "amc:api" in self.access_token["scope"]: + api_key = ap_storage_data[k]["client_id"] + access_token = entry.data.get(CONF_TOKEN) + if not "amc:api" in access_token["scope"]: async_create_issue( hass, DOMAIN, @@ -59,9 +57,7 @@ def __init__(self, hass: HomeAssistant, *, entry: ConfigEntry) -> None: translation_key="wrong_scope", ) low_energy = False - self.session = aioautomower.AutomowerSession( - self.api_key, self.access_token, low_energy - ) + self.session = aioautomower.AutomowerSession(api_key, access_token, low_energy) self.session.register_token_callback( lambda token: hass.config_entries.async_update_entry( entry, @@ -71,16 +67,15 @@ def __init__(self, hass: HomeAssistant, *, entry: ConfigEntry) -> None: async def _async_update_data(self) -> None: """Fetch data from Husqvarna.""" - provider = self.access_token["provider"] - token_type = self.access_token["token_type"] - access_token = self.access_token["access_token"] - rest = aioautomower.GetMowerData( - self.api_key, access_token, provider, token_type - ) try: - return await rest.async_mower_state() - except aioautomower.MowerApiConnectionsError as error: - _LOGGER.debug("Exception in updating Rest data: %s", error) + await self.session.connect() + except AsyncioTimeoutError as error: + _LOGGER.debug("Asyncio timeout: %s", error) + raise ConfigEntryNotReady from error + except Exception as error: + _LOGGER.debug("Exception in async_setup_entry: %s", error) + # If we haven't used the refresh_token (ie. been offline) for 10 days, + # we need to login using username and password in the config flow again. raise ConfigEntryAuthFailed from Exception @@ -93,20 +88,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, entry=entry, ) - - try: - coordinator.async_set_updated_data( - await coordinator.session.ws_and_token_session() - ) - except AsyncioTimeoutError as error: - _LOGGER.debug("Asyncio timeout: %s", error) - raise ConfigEntryNotReady from error - except Exception as error: - _LOGGER.debug("Exception in async_setup_entry: %s", error) - # If we haven't used the refresh_token (ie. been offline) for 10 days, - # we need to login using username and password in the config flow again. - raise ConfigEntryAuthFailed from Exception - await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) diff --git a/custom_components/husqvarna_automower/config_flow.py b/custom_components/husqvarna_automower/config_flow.py index e2d1276..0a6a549 100644 --- a/custom_components/husqvarna_automower/config_flow.py +++ b/custom_components/husqvarna_automower/config_flow.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant import config_entries, data_entry_flow from homeassistant.const import CONF_TOKEN -from homeassistant.core import async_get_hass, callback, HomeAssistant +from homeassistant.core import async_get_hass, callback from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers import selector import homeassistant.helpers.config_validation as cv @@ -122,7 +122,7 @@ def __init__(self, config_entry: config_entries.ConfigEntry) -> None: super().__init__() self.base_path = os.path.dirname(__file__) self.config_entry = config_entry - self.hass: HomeAssistant # pylint throws no member errors if we don't hint + self.hass = async_get_hass() self.mower_idx = [] for entity in self.hass.data[DOMAIN].keys(): diff --git a/custom_components/husqvarna_automower/entity.py b/custom_components/husqvarna_automower/entity.py index bd6ee82..40ebf6e 100644 --- a/custom_components/husqvarna_automower/entity.py +++ b/custom_components/husqvarna_automower/entity.py @@ -47,10 +47,7 @@ async def async_added_to_hass(self) -> None: """Call when entity about to be added to Home Assistant.""" await super().async_added_to_hass() self.coordinator.session.register_data_callback( - lambda _: self.coordinator.async_set_updated_data( - self.coordinator.session.data - ), - schedule_immediately=True, + lambda _: self.async_write_ha_state(), schedule_immediately=True ) async def async_will_remove_from_hass(self) -> None: diff --git a/custom_components/husqvarna_automower/manifest.json b/custom_components/husqvarna_automower/manifest.json index 61a8424..c068efd 100644 --- a/custom_components/husqvarna_automower/manifest.json +++ b/custom_components/husqvarna_automower/manifest.json @@ -14,18 +14,13 @@ "integration_type": "hub", "iot_class": "cloud_push", "issue_tracker": "https://github.com/Thomas55555/husqvarna_automower/issues", - "loggers": [ - "aioautomower", - "geopy", - "numpy", - "Pillow" - ], + "loggers": ["aioautomower", "geopy", "numpy", "Pillow"], "requirements": [ - "aioautomower==2023.7.1", + "aioautomower==2023.4.5", "geopy>=2.1.0", "numpy>=1.21.6", "Pillow>=9.1.1", "Shapely>=1.8.2" ], "version": "0.0.0" -} \ No newline at end of file +} diff --git a/custom_components/husqvarna_automower/tests/const.py b/custom_components/husqvarna_automower/tests/const.py index 704f91d..5de1bfe 100644 --- a/custom_components/husqvarna_automower/tests/const.py +++ b/custom_components/husqvarna_automower/tests/const.py @@ -34,46 +34,13 @@ # Single Mower Options AUTOMER_SM_CONFIG = { - "configured_zones": '{"front_garden": {"zone_coordinates": [[35.5408367, -82.5524521],' - " [35.5403893, -82.552613], [35.5399462, -82.5506738], [35.5403827, -82.5505236]," - ' [35.5408367, -82.5524521]], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0"],' - ' "color": [255, 0, 0], "name": "Front Garden", "display": true}, "west_italian_garden":' - ' {"zone_coordinates": [[35.5402452, -82.552951], [35.540075, -82.5530073],' - " [35.5399943, -82.5526425], [35.5401536, -82.5525835], [35.5402452, -82.552951]]," - ' "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0"], "color": [0, 255, 0],' - ' "name": "West Italian Garden", "display": true}, "east_italian_garden":' - ' {"zone_coordinates": [[35.5398415, -82.5512532], [35.5396822, -82.5513122],' - " [35.5395927, -82.550942], [35.5397498, -82.5508803], [35.5398415, -82.5512532]]," - ' "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0"], "color": [0, 0, 255],' - ' "name": "East Italian Garden", "display": true}, "shrub_garden": {"zone_coordinates":' - " [[35.5397978, -82.5531334], [35.539357, -82.553289], [35.5393198, -82.553128]," - " [35.5394028, -82.5530529], [35.5394443, -82.5529751], [35.5394639, -82.5528866]," - " [35.5394901, -82.5528303], [35.539645, -82.5529242], [35.5397629, -82.5529698]," - ' [35.5397978, -82.5531334]], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0"],' - ' "color": [100, 100, 0], "name": "Shrub Garden", "display": true}}', + "configured_zones": '{"front_garden": {"zone_coordinates": [[35.5408367, -82.5524521], [35.5403893, -82.552613], [35.5399462, -82.5506738], [35.5403827, -82.5505236], [35.5408367, -82.5524521]], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0"], "color": [255, 0, 0], "name": "Front Garden", "display": true}, "west_italian_garden": {"zone_coordinates": [[35.5402452, -82.552951], [35.540075, -82.5530073], [35.5399943, -82.5526425], [35.5401536, -82.5525835], [35.5402452, -82.552951]], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0"], "color": [0, 255, 0], "name": "West Italian Garden", "display": true}, "east_italian_garden": {"zone_coordinates": [[35.5398415, -82.5512532], [35.5396822, -82.5513122], [35.5395927, -82.550942], [35.5397498, -82.5508803], [35.5398415, -82.5512532]], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0"], "color": [0, 0, 255], "name": "East Italian Garden", "display": true}, "shrub_garden": {"zone_coordinates": [[35.5397978, -82.5531334], [35.539357, -82.553289], [35.5393198, -82.553128], [35.5394028, -82.5530529], [35.5394443, -82.5529751], [35.5394639, -82.5528866], [35.5394901, -82.5528303], [35.539645, -82.5529242], [35.5397629, -82.5529698], [35.5397978, -82.5531334]], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0"], "color": [100, 100, 0], "name": "Shrub Garden", "display": true}}', MWR_ONE_ID: MWR_ONE_CONFIG, } # Dual Mower Options AUTOMER_DM_CONFIG = { - "configured_zones": '{"front_garden": {"zone_coordinates": [[35.5408367, -82.5524521],' - " [35.5403893, -82.552613], [35.5399462, -82.5506738], [35.5403827, -82.5505236], " - '[35.5408367, -82.5524521]], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0",' - ' "1c7aec7b-06ff-462e-b307-7c6ae4469047"], "color": [255, 0, 0], "name": "Front Garden",' - ' "display": true}, "west_italian_garden": {"zone_coordinates": [[35.5402452, -82.552951],' - " [35.540075, -82.5530073], [35.5399943, -82.5526425], [35.5401536, -82.5525835], " - '[35.5402452, -82.552951]], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0"], ' - '"color": [0, 255, 0], "name": "West Italian Garden", "display": true}, ' - '"east_italian_garden": {"zone_coordinates": [[35.5398415, -82.5512532], ' - "[35.5396822, -82.5513122], [35.5395927, -82.550942], [35.5397498, -82.5508803]," - ' [35.5398415, -82.5512532]], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0"],' - ' "color": [0, 0, 255], "name": "East Italian Garden", "display": true}, "shrub_garden":' - ' {"zone_coordinates": [[35.5397978, -82.5531334], [35.539357, -82.553289],' - " [35.5393198, -82.553128], [35.5394028, -82.5530529], [35.5394443, -82.5529751]," - " [35.5394639, -82.5528866], [35.5394901, -82.5528303], [35.539645, -82.5529242]," - ' [35.5397629, -82.5529698], [35.5397978, -82.5531334]], "sel_mowers": ' - '["c7233734-b219-4287-a173-08e3643f89f0"], "color": [100, 100, 0], "name":' - ' "Shrub Garden", "display": true}}', + "configured_zones": '{"front_garden": {"zone_coordinates": [[35.5408367, -82.5524521], [35.5403893, -82.552613], [35.5399462, -82.5506738], [35.5403827, -82.5505236], [35.5408367, -82.5524521]], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0", "1c7aec7b-06ff-462e-b307-7c6ae4469047"], "color": [255, 0, 0], "name": "Front Garden", "display": true}, "west_italian_garden": {"zone_coordinates": [[35.5402452, -82.552951], [35.540075, -82.5530073], [35.5399943, -82.5526425], [35.5401536, -82.5525835], [35.5402452, -82.552951]], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0"], "color": [0, 255, 0], "name": "West Italian Garden", "display": true}, "east_italian_garden": {"zone_coordinates": [[35.5398415, -82.5512532], [35.5396822, -82.5513122], [35.5395927, -82.550942], [35.5397498, -82.5508803], [35.5398415, -82.5512532]], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0"], "color": [0, 0, 255], "name": "East Italian Garden", "display": true}, "shrub_garden": {"zone_coordinates": [[35.5397978, -82.5531334], [35.539357, -82.553289], [35.5393198, -82.553128], [35.5394028, -82.5530529], [35.5394443, -82.5529751], [35.5394639, -82.5528866], [35.5394901, -82.5528303], [35.539645, -82.5529242], [35.5397629, -82.5529698], [35.5397978, -82.5531334]], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0"], "color": [100, 100, 0], "name": "Shrub Garden", "display": true}}', MWR_ONE_ID: MWR_ONE_CONFIG, MWR_TWO_ID: MWR_TWO_CONFIG, } diff --git a/custom_components/husqvarna_automower/tests/test_application_credentials.py b/custom_components/husqvarna_automower/tests/test_application_credentials.py index 950f202..8f75983 100644 --- a/custom_components/husqvarna_automower/tests/test_application_credentials.py +++ b/custom_components/husqvarna_automower/tests/test_application_credentials.py @@ -1,17 +1,63 @@ """Tests for application credentials module.""" -from unittest.mock import MagicMock, patch +from copy import deepcopy +from unittest.mock import AsyncMock, MagicMock, patch import pytest +from aioautomower import AutomowerSession from homeassistant.components.application_credentials import AuthorizationServer +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from pytest_homeassistant_custom_component.common import MockConfigEntry from ..application_credentials import ( async_get_authorization_server, async_get_description_placeholders, ) -from ..const import HUSQVARNA_URL +from ..const import DOMAIN, HUSQVARNA_URL +from .const import AUTOMER_DM_CONFIG, AUTOMOWER_CONFIG_DATA, AUTOMOWER_SM_SESSION_DATA -from .test_common import setup_entity + +@pytest.mark.asyncio +async def setup_entity(hass: HomeAssistant): + """Set up entity and config entry""" + + options = deepcopy(AUTOMER_DM_CONFIG) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=AUTOMOWER_CONFIG_DATA, + options=options, + entry_id="automower_test", + title="Automower Test", + ) + + config_entry.add_to_hass(hass) + + session = deepcopy(AUTOMOWER_SM_SESSION_DATA) + + with patch( + "aioautomower.AutomowerSession", + return_value=AsyncMock( + name="AutomowerMockSession", + model=AutomowerSession, + data=session, + register_data_callback=MagicMock(), + unregister_data_callback=MagicMock(), + register_token_callback=MagicMock(), + connect=AsyncMock(), + action=AsyncMock(), + ), + ) as automower_session_mock: + automower_coordinator_mock = MagicMock( + name="MockCoordinator", session=automower_session_mock() + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + return config_entry @pytest.mark.asyncio diff --git a/custom_components/husqvarna_automower/tests/test_binary_sensor.py b/custom_components/husqvarna_automower/tests/test_binary_sensor.py index f7f28fe..aa68b7b 100644 --- a/custom_components/husqvarna_automower/tests/test_binary_sensor.py +++ b/custom_components/husqvarna_automower/tests/test_binary_sensor.py @@ -1,6 +1,11 @@ """Tests for binary sensor module.""" +from copy import deepcopy +from unittest.mock import AsyncMock, MagicMock, patch + import pytest +from aioautomower import AutomowerSession from homeassistant.core import HomeAssistant +from pytest_homeassistant_custom_component.common import MockConfigEntry from ..binary_sensor import ( AutomowerBatteryChargingBinarySensor, @@ -9,27 +14,59 @@ ) from ..const import DOMAIN, ERROR_STATES from .const import ( + AUTOMER_SM_CONFIG, + AUTOMOWER_CONFIG_DATA, + AUTOMOWER_SM_SESSION_DATA, MWR_ONE_ID, MWR_ONE_IDX, ) -from .test_common import setup_entity + +@pytest.mark.asyncio +async def setup_binary_sensor(hass: HomeAssistant, sensor_class): + """Set up binary sensor and config entry""" + + options = deepcopy(AUTOMER_SM_CONFIG) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=AUTOMOWER_CONFIG_DATA, + options=options, + entry_id="automower_test", + title="Automower Test", + ) + + config_entry.add_to_hass(hass) + + with patch( + "aioautomower.AutomowerSession", + return_value=AsyncMock( + name="AutomowerMockSession", + model=AutomowerSession, + data=AUTOMOWER_SM_SESSION_DATA, + register_data_callback=MagicMock(), + unregister_data_callback=MagicMock(), + ), + ) as automower_session_mock: + automower_coordinator_mock = MagicMock( + name="MockCoordinator", session=automower_session_mock() + ) + + binary_sensor = sensor_class(automower_coordinator_mock, MWR_ONE_IDX) + return binary_sensor, automower_coordinator_mock @pytest.mark.asyncio async def test_battery_charging_sensor(hass: HomeAssistant): """test AutomowerBatteryChargingBinarySensor""" + battery_sensor, automower_coordinator_mock = await setup_binary_sensor( + hass, AutomowerBatteryChargingBinarySensor + ) - await setup_entity(hass) - coordinator = hass.data[DOMAIN]["automower_test"] - - battery_sensor = AutomowerBatteryChargingBinarySensor(coordinator, MWR_ONE_IDX) - - # pylint: disable=protected-access assert battery_sensor._attr_unique_id == f"{MWR_ONE_ID}_battery_charging" assert battery_sensor.is_on is False - coordinator.session.data["data"][MWR_ONE_IDX]["attributes"]["mower"][ + automower_coordinator_mock.session.data["data"][MWR_ONE_IDX]["attributes"]["mower"][ "activity" ] = "CHARGING" @@ -39,17 +76,14 @@ async def test_battery_charging_sensor(hass: HomeAssistant): @pytest.mark.asyncio async def test_leaving_dock_sensor(hass: HomeAssistant): """test AutomowerLeavingDockBinarySensor""" + leaving_sensor, automower_coordinator_mock = await setup_binary_sensor( + hass, AutomowerLeavingDockBinarySensor + ) - await setup_entity(hass) - coordinator = hass.data[DOMAIN]["automower_test"] - - leaving_sensor = AutomowerLeavingDockBinarySensor(coordinator, MWR_ONE_IDX) - - # pylint: disable=protected-access assert leaving_sensor._attr_unique_id == f"{MWR_ONE_ID}_leaving_dock" assert leaving_sensor.is_on is False - coordinator.session.data["data"][MWR_ONE_IDX]["attributes"]["mower"][ + automower_coordinator_mock.session.data["data"][MWR_ONE_IDX]["attributes"]["mower"][ "activity" ] = "LEAVING" @@ -59,13 +93,10 @@ async def test_leaving_dock_sensor(hass: HomeAssistant): @pytest.mark.asyncio async def test_error_sensor(hass: HomeAssistant): """test AutomowerErrorBinarySensor""" + error_sensor, automower_coordinator_mock = await setup_binary_sensor( + hass, AutomowerErrorBinarySensor + ) - await setup_entity(hass) - coordinator = hass.data[DOMAIN]["automower_test"] - - error_sensor = AutomowerErrorBinarySensor(coordinator, MWR_ONE_IDX) - - # pylint: disable=protected-access assert error_sensor._attr_unique_id == f"{MWR_ONE_ID}_error" assert error_sensor.is_on is False @@ -74,7 +105,7 @@ async def test_error_sensor(hass: HomeAssistant): "description": "No Error", } - coordinator.session.data["data"][MWR_ONE_IDX]["attributes"]["mower"][ + automower_coordinator_mock.session.data["data"][MWR_ONE_IDX]["attributes"]["mower"][ "errorCode" ] = 0 @@ -84,9 +115,9 @@ async def test_error_sensor(hass: HomeAssistant): } for e_state in ERROR_STATES: - coordinator.session.data["data"][MWR_ONE_IDX]["attributes"]["mower"][ - "state" - ] = e_state + automower_coordinator_mock.session.data["data"][MWR_ONE_IDX]["attributes"][ + "mower" + ]["state"] = e_state assert error_sensor.is_on is True assert error_sensor.extra_state_attributes == { diff --git a/custom_components/husqvarna_automower/tests/test_calendar.py b/custom_components/husqvarna_automower/tests/test_calendar.py index 5b4f3a4..1025a98 100644 --- a/custom_components/husqvarna_automower/tests/test_calendar.py +++ b/custom_components/husqvarna_automower/tests/test_calendar.py @@ -1,21 +1,69 @@ """Tests for calendar module.""" +from copy import deepcopy from datetime import datetime from unittest.mock import AsyncMock, MagicMock, patch import pytest import voluptuous as vol +from aioautomower import AutomowerSession from aiohttp import ClientResponseError from geopy import Location +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from pytest_homeassistant_custom_component.common import MockConfigEntry from ..calendar import AutomowerCalendar from ..const import DOMAIN from .const import ( + AUTOMER_DM_CONFIG, + AUTOMOWER_CONFIG_DATA, + AUTOMOWER_SM_SESSION_DATA, MWR_ONE_ID, MWR_ONE_IDX, ) -from .test_common import setup_entity + +@pytest.mark.asyncio +async def setup_entity(hass: HomeAssistant): + """Set up entity and config entry""" + + options = deepcopy(AUTOMER_DM_CONFIG) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=AUTOMOWER_CONFIG_DATA, + options=options, + entry_id="automower_test", + title="Automower Test", + ) + + config_entry.add_to_hass(hass) + + session = deepcopy(AUTOMOWER_SM_SESSION_DATA) + + with patch( + "aioautomower.AutomowerSession", + return_value=AsyncMock( + name="AutomowerMockSession", + model=AutomowerSession, + data=session, + register_data_callback=MagicMock(), + unregister_data_callback=MagicMock(), + register_token_callback=MagicMock(), + connect=AsyncMock(), + action=AsyncMock(), + ), + ) as automower_session_mock: + automower_coordinator_mock = MagicMock( + name="MockCoordinator", session=automower_session_mock() + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + return config_entry @pytest.mark.asyncio @@ -25,7 +73,6 @@ async def test_calendar(hass: HomeAssistant): coordinator = hass.data[DOMAIN]["automower_test"] calendar = AutomowerCalendar(coordinator, MWR_ONE_IDX) - # pylint: disable=protected-access assert calendar._attr_unique_id == f"{MWR_ONE_ID}_calendar" # Not connected @@ -33,19 +80,18 @@ async def test_calendar(hass: HomeAssistant): "connected" ] = False - assert calendar.available is False + assert calendar.available == False # Connected coordinator.session.data["data"][MWR_ONE_IDX]["attributes"]["metadata"][ "connected" ] = True - assert calendar.available is True + assert calendar.available == True # Get calendar events location_result = Location( - "Italian Garden, Biltmore Estate Path, Buncombe County," - " North Carolina, 28803, United States", + "Italian Garden, Biltmore Estate Path, Buncombe County, North Carolina, 28803, United States", (35.5399226, -82.55193188763246, 0.0), { "place_id": 266519807, @@ -54,8 +100,7 @@ async def test_calendar(hass: HomeAssistant): "osm_id": 830192286, "lat": "35.5399226", "lon": "-82.55193188763246", - "display_name": "Italian Garden, Biltmore Estate Path, " - "Buncombe County, North Carolina, 28803, United States", + "display_name": "Italian Garden, Biltmore Estate Path, Buncombe County, North Carolina, 28803, United States", "address": { "leisure": "Italian Garden", "road": "Biltmore Estate Path", @@ -84,8 +129,7 @@ async def test_calendar(hass: HomeAssistant): # No house number in return address location_result = Location( - "Italian Garden, Biltmore Estate Path, Buncombe County," - " North Carolina, 28803, United States", + "Italian Garden, Biltmore Estate Path, Buncombe County, North Carolina, 28803, United States", (35.5399226, -82.55193188763246, 0.0), { "place_id": 266519807, @@ -94,8 +138,7 @@ async def test_calendar(hass: HomeAssistant): "osm_id": 830192286, "lat": "35.5399226", "lon": "-82.55193188763246", - "display_name": "Italian Garden, Biltmore Estate Path, Buncombe County," - " North Carolina, 28803, United States", + "display_name": "Italian Garden, Biltmore Estate Path, Buncombe County, North Carolina, 28803, United States", "address": { "leisure": "Italian Garden", "road": "Biltmore Estate Path", @@ -125,8 +168,7 @@ async def test_calendar(hass: HomeAssistant): # No postions, Index error location_result = Location( - "Italian Garden, Biltmore Estate Path, Buncombe County," - " North Carolina, 28803, United States", + "Italian Garden, Biltmore Estate Path, Buncombe County, North Carolina, 28803, United States", (35.5399226, -82.55193188763246, 0.0), { "place_id": 266519807, @@ -135,8 +177,7 @@ async def test_calendar(hass: HomeAssistant): "osm_id": 830192286, "lat": "35.5399226", "lon": "-82.55193188763246", - "display_name": "Italian Garden, Biltmore Estate Path, Buncombe County," - " North Carolina, 28803, United States", + "display_name": "Italian Garden, Biltmore Estate Path, Buncombe County, North Carolina, 28803, United States", "address": { "leisure": "Italian Garden", "road": "Biltmore Estate Path", @@ -184,7 +225,6 @@ async def test_async_parse_to_husqvarna_string(hass: HomeAssistant): coordinator = hass.data[DOMAIN]["automower_test"] calendar = AutomowerCalendar(coordinator, MWR_ONE_IDX) - # pylint: disable=protected-access assert calendar._attr_unique_id == f"{MWR_ONE_ID}_calendar" # Parse to Husqvarna String @@ -233,7 +273,6 @@ async def test_aysnc_send_command_to_mower(hass: HomeAssistant): coordinator = hass.data[DOMAIN]["automower_test"] calendar = AutomowerCalendar(coordinator, MWR_ONE_IDX) - # pylint: disable=protected-access assert calendar._attr_unique_id == f"{MWR_ONE_ID}_calendar" task_list = [ @@ -254,9 +293,7 @@ async def test_aysnc_send_command_to_mower(hass: HomeAssistant): await calendar.aysnc_send_command_to_mower(task_list) coordinator.session.action.assert_awaited_once_with( MWR_ONE_ID, - '{"data": {"type": "calendar", "attributes": {"tasks": [{"start": 480, "duration": 60,' - ' "friday": false, "monday": true, "saturday": false, "sunday": false, "wednesday":' - ' true, "thursday": false, "tuesday": true}]}}}', + '{"data": {"type": "calendar", "attributes": {"tasks": [{"start": 480, "duration": 60, "friday": false, "monday": true, "saturday": false, "sunday": false, "wednesday": true, "thursday": false, "tuesday": true}]}}}', "calendar", ) @@ -275,7 +312,6 @@ async def test_aysnc_edit_events(hass: HomeAssistant): coordinator = hass.data[DOMAIN]["automower_test"] calendar = AutomowerCalendar(coordinator, MWR_ONE_IDX) - # pylint: disable=protected-access assert calendar._attr_unique_id == f"{MWR_ONE_ID}_calendar" event = { @@ -288,14 +324,7 @@ async def test_aysnc_edit_events(hass: HomeAssistant): await calendar.async_create_event(**event) coordinator.session.action.assert_awaited_once_with( MWR_ONE_ID, - '{"data": {"type": "calendar", "attributes": {"tasks": [{"start": 1140,' - ' "duration": 300, "monday": true, "tuesday": false, "wednesday": true,' - ' "thursday": false, "friday": true, "saturday": false, "sunday": false},' - ' {"start": 0, "duration": 480, "monday": false, "tuesday": true, ' - '"wednesday": false, "thursday": true, "friday": false, "saturday": true,' - ' "sunday": false}, {"start": 480, "duration": 60, "monday": true,' - ' "tuesday": true, "wednesday": true, "thursday": false, "friday": false,' - ' "saturday": false, "sunday": false}]}}}', + '{"data": {"type": "calendar", "attributes": {"tasks": [{"start": 1140, "duration": 300, "monday": true, "tuesday": false, "wednesday": true, "thursday": false, "friday": true, "saturday": false, "sunday": false}, {"start": 0, "duration": 480, "monday": false, "tuesday": true, "wednesday": false, "thursday": true, "friday": false, "saturday": true, "sunday": false}, {"start": 480, "duration": 60, "monday": true, "tuesday": true, "wednesday": true, "thursday": false, "friday": false, "saturday": false, "sunday": false}]}}}', "calendar", ) @@ -305,12 +334,7 @@ async def test_aysnc_edit_events(hass: HomeAssistant): await calendar.async_update_event("0", event) coordinator.session.action.assert_awaited_once_with( MWR_ONE_ID, - '{"data": {"type": "calendar", "attributes": {"tasks": [{"start": 480,' - ' "duration": 60, "monday": true, "tuesday": true, "wednesday": true,' - ' "thursday": false, "friday": false, "saturday": false, "sunday": false},' - ' {"start": 0, "duration": 480, "monday": false, "tuesday": true,' - ' "wednesday": false, "thursday": true, "friday": false, ' - '"saturday": true, "sunday": false}]}}}', + '{"data": {"type": "calendar", "attributes": {"tasks": [{"start": 480, "duration": 60, "monday": true, "tuesday": true, "wednesday": true, "thursday": false, "friday": false, "saturday": false, "sunday": false}, {"start": 0, "duration": 480, "monday": false, "tuesday": true, "wednesday": false, "thursday": true, "friday": false, "saturday": true, "sunday": false}]}}}', "calendar", ) @@ -320,9 +344,7 @@ async def test_aysnc_edit_events(hass: HomeAssistant): await calendar.async_delete_event("0") coordinator.session.action.assert_awaited_once_with( MWR_ONE_ID, - '{"data": {"type": "calendar", "attributes": {"tasks": [{"start": 0, "duration": 480,' - ' "monday": false, "tuesday": true, "wednesday": false, "thursday": true,' - ' "friday": false, "saturday": true, "sunday": false}]}}}', + '{"data": {"type": "calendar", "attributes": {"tasks": [{"start": 0, "duration": 480, "monday": false, "tuesday": true, "wednesday": false, "thursday": true, "friday": false, "saturday": true, "sunday": false}]}}}', "calendar", ) diff --git a/custom_components/husqvarna_automower/tests/test_common.py b/custom_components/husqvarna_automower/tests/test_common.py deleted file mode 100644 index cfa61a0..0000000 --- a/custom_components/husqvarna_automower/tests/test_common.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Common test functions module.""" -from copy import deepcopy -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest -from aioautomower import AutomowerSession, GetMowerData -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) -from pytest_homeassistant_custom_component.common import MockConfigEntry - -from ..const import DOMAIN -from .const import ( - AUTOMER_SM_CONFIG, - AUTOMER_DM_CONFIG, - AUTOMOWER_CONFIG_DATA, - AUTOMOWER_SM_SESSION_DATA, - AUTOMOWER_DM_SESSION_DATA, -) - - -@pytest.mark.asyncio -async def setup_entity(hass: HomeAssistant, dual_mower: bool = False, conf_version=1): - """Set up entity and config entry""" - - if dual_mower: - options = deepcopy(AUTOMER_DM_CONFIG) - else: - options = deepcopy(AUTOMER_SM_CONFIG) - - config_entry = MockConfigEntry( - domain=DOMAIN, - data=AUTOMOWER_CONFIG_DATA, - options=options, - entry_id="automower_test", - title="Automower Test", - version=conf_version, - ) - - config_entry.add_to_hass(hass) - - if dual_mower: - session = deepcopy(AUTOMOWER_DM_SESSION_DATA) - else: - session = deepcopy(AUTOMOWER_SM_SESSION_DATA) - - with patch( - "aioautomower.AutomowerSession", - return_value=AsyncMock( - name="AutomowerMockSession", - model=AutomowerSession, - data=session, - register_data_callback=MagicMock(), - unregister_data_callback=MagicMock(), - register_token_callback=MagicMock(), - connect=AsyncMock(), - action=AsyncMock(), - ), - ): - with patch( - "aioautomower.GetMowerData", - return_value=AsyncMock(name="GetMowerMock", model=GetMowerData, data={}), - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - - return config_entry - - -async def configure_application_credentials(hass: HomeAssistant): - """Configure application credentials""" - app_cred_config_entry = MockConfigEntry( - domain="application_credentials", - data={}, - entry_id="application_credentials", - title="Application Credentials", - ) - app_cred_config_entry.add_to_hass(hass) - - await hass.config_entries.async_setup(app_cred_config_entry.entry_id) - - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential( - "test_client_id", - "test_config_secret", - ), - ) diff --git a/custom_components/husqvarna_automower/tests/test_config_flow.py b/custom_components/husqvarna_automower/tests/test_config_flow.py index 79fdbe4..58968a9 100644 --- a/custom_components/husqvarna_automower/tests/test_config_flow.py +++ b/custom_components/husqvarna_automower/tests/test_config_flow.py @@ -1,12 +1,14 @@ """Tests for config flow module.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, MagicMock, patch +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant - from homeassistant.data_entry_flow import FlowResultType +from pytest_homeassistant_custom_component.common import MockConfigEntry from ..const import ( ADD_IMAGES, + DOMAIN, ENABLE_IMAGE, GPS_BOTTOM_RIGHT, GPS_TOP_LEFT, @@ -27,12 +29,12 @@ ) from .const import ( AUTOMER_DM_CONFIG, + AUTOMOWER_CONFIG_DATA, + AUTOMOWER_DM_SESSION_DATA, MWR_ONE_ID, MWR_TWO_ID, ) -from .test_common import setup_entity - def get_suggested(schema, key): """Get suggested value for key in voluptuous schema.""" @@ -42,17 +44,33 @@ def get_suggested(schema, key): return None return k.description["suggested_value"] # Wanted key absent from schema - raise KeyError + raise Exception async def test_options_init(hass: HomeAssistant) -> None: """Test option flow init""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=AUTOMOWER_CONFIG_DATA, + options={}, + entry_id="automower_test", + title="Automower Test", + ) with patch( - "custom_components.husqvarna_automower.tests.test_common.AUTOMER_DM_CONFIG", - {}, + "aioautomower.AutomowerSession", + return_value=AsyncMock( + register_token_callback=MagicMock(), + connect=AsyncMock(), + data=AUTOMOWER_DM_SESSION_DATA, + register_data_callback=MagicMock(), + unregister_data_callback=MagicMock(), + ), ): - config_entry = await setup_entity(hass, dual_mower=True) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == FlowResultType.MENU assert result["step_id"] == "select" @@ -60,17 +78,38 @@ async def test_options_init(hass: HomeAssistant) -> None: async def test_options_image_config_existing_options(hass: HomeAssistant) -> None: """Test Image Config option flow.""" - config_entry = await setup_entity(hass, dual_mower=True) + config_entry = MockConfigEntry( + domain=DOMAIN, + data=AUTOMOWER_CONFIG_DATA, + options=AUTOMER_DM_CONFIG, + entry_id="automower_test", + title="Automower Test", + ) + with patch( + "aioautomower.AutomowerSession", + return_value=AsyncMock( + register_token_callback=MagicMock(), + connect=AsyncMock(), + data=AUTOMOWER_DM_SESSION_DATA, + register_data_callback=MagicMock(), + unregister_data_callback=MagicMock(), + ), + ): + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - result = await hass.config_entries.options.async_init(config_entry.entry_id) + result = await hass.config_entries.options.async_init(config_entry.entry_id) - result = await hass.config_entries.options.async_configure( - result["flow_id"], {"next_step_id": "image_select"} - ) + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"next_step_id": "image_select"} + ) - result = await hass.config_entries.options.async_configure( - result["flow_id"], {"selected_image": "Test Mower 1"} - ) + result = await hass.config_entries.options.async_configure( + result["flow_id"], {"selected_image": "Test Mower 1"} + ) async def test_options_image_config_existing_options_bad_zone( @@ -79,12 +118,28 @@ async def test_options_image_config_existing_options_bad_zone( """Test Image Config option flow where the configured zones is not a dict.""" options = AUTOMER_DM_CONFIG.copy() options["configured_zones"] = "[]" - + config_entry = MockConfigEntry( + domain=DOMAIN, + data=AUTOMOWER_CONFIG_DATA, + options=options, + entry_id="automower_test", + title="Automower Test", + ) with patch( - "custom_components.husqvarna_automower.tests.test_common.AUTOMER_DM_CONFIG", - options, + "aioautomower.AutomowerSession", + return_value=AsyncMock( + register_token_callback=MagicMock(), + connect=AsyncMock(), + data=AUTOMOWER_DM_SESSION_DATA, + register_data_callback=MagicMock(), + unregister_data_callback=MagicMock(), + ), ): - config_entry = await setup_entity(hass, dual_mower=True) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -99,22 +154,36 @@ async def test_options_image_config_existing_options_bad_zone( async def test_options_image_config(hass: HomeAssistant) -> None: """Test Image Config option flow.""" - + config_entry = MockConfigEntry( + domain=DOMAIN, + data=AUTOMOWER_CONFIG_DATA, + options={}, + entry_id="automower_test", + title="Automower Test", + ) with patch( - "custom_components.husqvarna_automower.tests.test_common.AUTOMER_DM_CONFIG", - {}, + "aioautomower.AutomowerSession", + return_value=AsyncMock( + register_token_callback=MagicMock(), + connect=AsyncMock(), + data=AUTOMOWER_DM_SESSION_DATA, + register_data_callback=MagicMock(), + unregister_data_callback=MagicMock(), + ), ): - config_entry = await setup_entity(hass, dual_mower=True) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 result = await hass.config_entries.options.async_init(config_entry.entry_id) - await hass.async_block_till_done() assert result["type"] == FlowResultType.MENU assert result["step_id"] == "select" result = await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": "image_select"} ) - await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM assert result["step_id"] == "image_select" @@ -125,7 +194,6 @@ async def test_options_image_config(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_configure( result["flow_id"], {"selected_image": "Test Mower 1"} ) - await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM assert result["step_id"] == "image_config" @@ -141,13 +209,11 @@ async def test_options_image_config(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_configure( result["flow_id"], {ENABLE_IMAGE: False} ) - await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY # Restart flow result = await hass.config_entries.options.async_init(config_entry.entry_id) - await hass.async_block_till_done() assert result["type"] == FlowResultType.MENU assert result["step_id"] == "select" @@ -163,7 +229,7 @@ async def test_options_image_config(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_configure( result["flow_id"], {ENABLE_IMAGE: True} ) - await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "image_config" assert result["errors"][GPS_BOTTOM_RIGHT] == "points_match" @@ -177,7 +243,7 @@ async def test_options_image_config(hass: HomeAssistant) -> None: GPS_TOP_LEFT: "235.5411008,-82.5527418", }, ) - await hass.async_block_till_done() + assert result["errors"] == {GPS_TOP_LEFT: "not_wgs84"} # Enable Image, provide invalid bottom right point @@ -189,7 +255,7 @@ async def test_options_image_config(hass: HomeAssistant) -> None: GPS_TOP_LEFT: "35.5411008,-82.5527418", }, ) - await hass.async_block_till_done() + assert result["errors"] == {GPS_BOTTOM_RIGHT: "not_wgs84"} # Enable Image, provide valid points, bad path for mower @@ -199,11 +265,10 @@ async def test_options_image_config(hass: HomeAssistant) -> None: ENABLE_IMAGE: True, GPS_BOTTOM_RIGHT: "35.539442,-82.5504646", GPS_TOP_LEFT: "35.5411008,-82.5527418", - MOWER_IMG_PATH: "custom_components/husqvarna_automower" - "/tests/resources/missing.png", + MOWER_IMG_PATH: "custom_components/husqvarna_automower/tests/resources/missing.png", }, ) - await hass.async_block_till_done() + assert result["errors"] == {MOWER_IMG_PATH: "not_file"} # Enable Image, provide valid points, bad image for mower @@ -213,11 +278,10 @@ async def test_options_image_config(hass: HomeAssistant) -> None: ENABLE_IMAGE: True, GPS_BOTTOM_RIGHT: "35.539442,-82.5504646", GPS_TOP_LEFT: "35.5411008,-82.5527418", - MOWER_IMG_PATH: "custom_components/husqvarna_automower" - "/tests/resources/bad_image.png", + MOWER_IMG_PATH: "custom_components/husqvarna_automower/tests/resources/bad_image.png", }, ) - await hass.async_block_till_done() + assert result["errors"] == {MOWER_IMG_PATH: "not_image"} # Enable Image, provide valid points, bad path for map @@ -227,11 +291,10 @@ async def test_options_image_config(hass: HomeAssistant) -> None: ENABLE_IMAGE: True, GPS_BOTTOM_RIGHT: "35.539442,-82.5504646", GPS_TOP_LEFT: "35.5411008,-82.5527418", - MAP_IMG_PATH: "custom_components/husqvarna_automower" - "/tests/resources/missing.png", + MAP_IMG_PATH: "custom_components/husqvarna_automower/tests/resources/missing.png", }, ) - await hass.async_block_till_done() + assert result["errors"] == {MAP_IMG_PATH: "not_file"} # Enable Image, provide valid points, bad image for map @@ -241,11 +304,10 @@ async def test_options_image_config(hass: HomeAssistant) -> None: ENABLE_IMAGE: True, GPS_BOTTOM_RIGHT: "35.539442,-82.5504646", GPS_TOP_LEFT: "35.5411008,-82.5527418", - MAP_IMG_PATH: "custom_components/husqvarna_automower/" - "tests/resources/bad_image.png", + MAP_IMG_PATH: "custom_components/husqvarna_automower/tests/resources/bad_image.png", }, ) - await hass.async_block_till_done() + assert result["errors"] == {MAP_IMG_PATH: "not_image"} # Enable Image, provide valid points, bad color @@ -258,7 +320,7 @@ async def test_options_image_config(hass: HomeAssistant) -> None: MAP_PATH_COLOR: "-100, 0, 0", }, ) - await hass.async_block_till_done() + assert result["errors"] == {MAP_PATH_COLOR: "color_error"} # Enable Image, provide valid points, bad rotation @@ -271,7 +333,7 @@ async def test_options_image_config(hass: HomeAssistant) -> None: MAP_IMG_ROTATION: -500, }, ) - await hass.async_block_till_done() + assert result["errors"] == {MAP_IMG_ROTATION: "rotation_error"} # Enable Image, provide valid corner points, bad home point @@ -284,7 +346,7 @@ async def test_options_image_config(hass: HomeAssistant) -> None: HOME_LOCATION: "35.54028774,-282.5526962", }, ) - await hass.async_block_till_done() + assert result["errors"] == {HOME_LOCATION: "not_wgs84"} # Enable Image, provide valid points @@ -297,17 +359,32 @@ async def test_options_image_config(hass: HomeAssistant) -> None: HOME_LOCATION: "35.54028774,-82.5526962", }, ) - await hass.async_block_till_done() + assert "errors" not in result async def test_options_zone_config(hass: HomeAssistant) -> None: """Test Zone Config option flow (geofence_init).""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=AUTOMOWER_CONFIG_DATA, + options={}, + entry_id="automower_test", + title="Automower Test", + ) with patch( - "custom_components.husqvarna_automower.tests.test_common.AUTOMER_DM_CONFIG", - {}, + "aioautomower.AutomowerSession", + return_value=AsyncMock( + register_token_callback=MagicMock(), + connect=AsyncMock(), + data=AUTOMOWER_DM_SESSION_DATA, + register_data_callback=MagicMock(), + unregister_data_callback=MagicMock(), + ), ): - config_entry = await setup_entity(hass, dual_mower=True) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == FlowResultType.MENU @@ -341,8 +418,7 @@ async def test_options_zone_config(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_configure( result["flow_id"], { - ZONE_COORD: "35.5408367,-82.5524521 35.5403893,-82.552613;35.5399462," - "-82.5506738;35.5403827,-82.5505236;35.5408367,-82.5524521", + ZONE_COORD: "35.5408367,-82.5524521 35.5403893,-82.552613;35.5399462,-82.5506738;35.5403827,-82.5505236;35.5408367,-82.5524521", ZONE_NAME: "Front Garden", ZONE_COLOR: "255,0,0", ZONE_DISPLAY: True, @@ -376,8 +452,7 @@ async def test_options_zone_config(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_configure( result["flow_id"], { - ZONE_COORD: "35.5408367,-82.5524521;35.5403893,-82.552613;35.5399462," - "-82.5506738;35.5403827,-82.5505236;35.5408367,-82.5524521", + ZONE_COORD: "35.5408367,-82.5524521;35.5403893,-82.552613;35.5399462,-82.5506738;35.5403827,-82.5505236;35.5408367,-82.5524521", ZONE_NAME: "Front Garden", ZONE_COLOR: "500,0,0", ZONE_DISPLAY: True, @@ -394,9 +469,7 @@ async def test_options_zone_config(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_configure( result["flow_id"], { - ZONE_COORD: "35.5408367,-82.5524521;35.5403893," - "-82.552613;35.5399462,-82.5506738;35.5403827," - "-82.5505236;35.5408367,-82.5524521", + ZONE_COORD: "35.5408367,-82.5524521;35.5403893,-82.552613;35.5399462,-82.5506738;35.5403827,-82.5505236;35.5408367,-82.5524521", ZONE_NAME: "Front Garden", ZONE_COLOR: "255,0,0", ZONE_DISPLAY: True, @@ -413,8 +486,7 @@ async def test_options_zone_config(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_configure( result["flow_id"], { - ZONE_COORD: "35.5408367,-82.5524521;35.5403893,-82.552613;35.5399462," - "-82.5506738;35.5403827,-82.5505236;35.5408367,-82.5524521", + ZONE_COORD: "35.5408367,-82.5524521;35.5403893,-82.552613;35.5399462,-82.5506738;35.5403827,-82.5505236;35.5408367,-82.5524521", ZONE_NAME: "Front Garden", ZONE_COLOR: "255,0,0", ZONE_DISPLAY: True, @@ -422,7 +494,7 @@ async def test_options_zone_config(hass: HomeAssistant) -> None: }, ) - assert result["errors"] is None + assert result["errors"] == None assert result["type"] == FlowResultType.FORM assert result["step_id"] == "geofence_init" @@ -470,7 +542,7 @@ async def test_options_zone_config(hass: HomeAssistant) -> None: {ZONE_DEL: True}, ) - assert result["errors"] is None + assert result["errors"] == None assert result["type"] == FlowResultType.FORM assert result["step_id"] == "geofence_init" diff --git a/custom_components/husqvarna_automower/tests/test_device_tracker.py b/custom_components/husqvarna_automower/tests/test_device_tracker.py index 3c981b0..98fe59b 100644 --- a/custom_components/husqvarna_automower/tests/test_device_tracker.py +++ b/custom_components/husqvarna_automower/tests/test_device_tracker.py @@ -1,20 +1,67 @@ """Tests for device tracker module.""" from copy import deepcopy -from unittest.mock import patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest +from aioautomower import AutomowerSession from homeassistant.components.device_tracker import SourceType +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from pytest_homeassistant_custom_component.common import MockConfigEntry from ..const import DOMAIN -from ..device_tracker import AutomowerTracker +from ..device_tracker import AutomowerTracker, async_setup_entry from .const import ( + AUTOMER_DM_CONFIG, + AUTOMOWER_CONFIG_DATA, AUTOMOWER_SM_SESSION_DATA, MWR_ONE_ID, MWR_ONE_IDX, ) +from .test_init import configure_application_credentials -from .test_common import setup_entity + +@pytest.mark.asyncio +async def setup_device_tracker(hass: HomeAssistant, empty_positions: bool = False): + """Set up device tracker and config entry""" + + await configure_application_credentials(hass) + + options = AUTOMER_DM_CONFIG + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=AUTOMOWER_CONFIG_DATA, + options=options, + entry_id="automower_test", + title="Automower Test", + ) + + config_entry.add_to_hass(hass) + + session_data = deepcopy(AUTOMOWER_SM_SESSION_DATA) + + if empty_positions: + session_data["data"][MWR_ONE_IDX]["attributes"]["positions"] = [] + + with patch( + "aioautomower.AutomowerSession", + return_value=AsyncMock( + name="AutomowerMockSession", + model=AutomowerSession, + data=session_data, + register_token_callback=MagicMock(), + connect=AsyncMock(), + register_data_callback=MagicMock(), + unregister_data_callback=MagicMock(), + ), + ) as automower_session_mock: + automower_coordinator_mock = MagicMock( + name="MockCoordinator", session=automower_session_mock() + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() @pytest.mark.asyncio @@ -22,27 +69,19 @@ async def test_device_tracker_setup_no_pos(hass: HomeAssistant): """test device tracker without positions.""" # Without positions - session = deepcopy(AUTOMOWER_SM_SESSION_DATA) - session["data"][MWR_ONE_IDX]["attributes"]["positions"] = [] - - with patch( - "custom_components.husqvarna_automower.tests.test_common.AUTOMOWER_SM_SESSION_DATA", - session, - ): - await setup_entity(hass) - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + await setup_device_tracker(hass, empty_positions=True) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 coordinator = hass.data[DOMAIN]["automower_test"] - AutomowerTracker(coordinator, MWR_ONE_IDX) + device_tracker = AutomowerTracker(coordinator, MWR_ONE_IDX) @pytest.mark.asyncio async def test_device_tracker_pos(hass: HomeAssistant): """test device tracker with positions.""" - await setup_entity(hass) + await setup_device_tracker(hass, empty_positions=False) coordinator = hass.data[DOMAIN]["automower_test"] device_tracker = AutomowerTracker(coordinator, MWR_ONE_IDX) - # pylint: disable=protected-access assert device_tracker._attr_unique_id == f"{MWR_ONE_ID}_dt" assert device_tracker.source_type == SourceType.GPS assert ( diff --git a/custom_components/husqvarna_automower/tests/test_diagnostics.py b/custom_components/husqvarna_automower/tests/test_diagnostics.py index 8bcd5ff..74127d5 100644 --- a/custom_components/husqvarna_automower/tests/test_diagnostics.py +++ b/custom_components/husqvarna_automower/tests/test_diagnostics.py @@ -1,48 +1,63 @@ """Test for diagnostics module.""" +from unittest.mock import AsyncMock, MagicMock, patch + import pytest from homeassistant.core import HomeAssistant +from pytest_homeassistant_custom_component.common import MockConfigEntry +from ..const import DOMAIN from ..diagnostics import TO_REDACT, async_get_config_entry_diagnostics - -from .test_common import setup_entity +from .const import AUTOMER_SM_CONFIG, AUTOMOWER_CONFIG_DATA @pytest.mark.asyncio async def test_redact(hass: HomeAssistant): """test automower initialization""" - config_entry = await setup_entity(hass) - - diag_data = await async_get_config_entry_diagnostics(hass, config_entry) - - redacted = [] - # pylint: disable=invalid-name - for k, v in diag_data.get("config_entry").get("data").items(): - if k in TO_REDACT: # pylint: disable=invalid-name - assert v.get(k) == "**REDACTED**" # pylint: disable=invalid-name - redacted.append(k) # pylint: disable=invalid-name - - # pylint: disable=invalid-name - for k, v in diag_data.get("config_entry").get("options").items(): - if k in TO_REDACT: # pylint: disable=invalid-name - assert v == "**REDACTED**" # pylint: disable=invalid-name - redacted.append(k) - if isinstance(v, dict): - for k2, v2 in v.items(): # pylint: disable=invalid-name - if k2 in TO_REDACT: # pylint: disable=invalid-name - assert v2 == "**REDACTED**" # pylint: disable=invalid-name - redacted.append(k2) # pylint: disable=invalid-name - - assert ( - set(TO_REDACT) - .difference(redacted) - .issubset( - set( - [ - "positions", - "refresh_token", - "access_token", - ] + config_entry = MockConfigEntry( + domain=DOMAIN, + data=AUTOMOWER_CONFIG_DATA, + options=AUTOMER_SM_CONFIG, + entry_id="automower_test", + title="Automower Test", + ) + config_entry.add_to_hass(hass) + + with patch( + "aioautomower.AutomowerSession", + return_value=AsyncMock(register_token_callback=MagicMock()), + ) as mock_aioautomower_session: + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + diag_data = await async_get_config_entry_diagnostics(hass, config_entry) + + redacted = [] + for k, v in diag_data.get("config_entry").get("data").items(): + if k in TO_REDACT: + assert v.get(k) == "**REDACTED**" + redacted.append(k) + + for k, v in diag_data.get("config_entry").get("options").items(): + if k in TO_REDACT: + assert v == "**REDACTED**" + redacted.append(k) + if isinstance(v, dict): + for k2, v2 in v.items(): + if k2 in TO_REDACT: + assert v2 == "**REDACTED**" + redacted.append(k2) + + assert ( + set(TO_REDACT) + .difference(redacted) + .issubset( + set( + [ + "positions", + "refresh_token", + "access_token", + ] + ) ) ) - ) diff --git a/custom_components/husqvarna_automower/tests/test_entity.py b/custom_components/husqvarna_automower/tests/test_entity.py index 7f77926..9f170c4 100644 --- a/custom_components/husqvarna_automower/tests/test_entity.py +++ b/custom_components/husqvarna_automower/tests/test_entity.py @@ -1,20 +1,68 @@ """Tests for entity module.""" +from copy import deepcopy from datetime import datetime +from unittest.mock import AsyncMock, MagicMock, patch import pytest +from aioautomower import AutomowerSession from dateutil import tz +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from pytest_homeassistant_custom_component.common import MockConfigEntry from ..const import DOMAIN from ..entity import AutomowerEntity from .const import ( + AUTOMER_DM_CONFIG, + AUTOMOWER_CONFIG_DATA, + AUTOMOWER_SM_SESSION_DATA, MWR_ONE_ID, MWR_ONE_IDX, AUTOMOWER_ERROR_SESSION_DATA, ) -from .test_common import setup_entity + +@pytest.mark.asyncio +async def setup_entity(hass: HomeAssistant): + """Set up entity and config entry""" + + options = deepcopy(AUTOMER_DM_CONFIG) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=AUTOMOWER_CONFIG_DATA, + options=options, + entry_id="automower_test", + title="Automower Test", + ) + + config_entry.add_to_hass(hass) + + session = deepcopy(AUTOMOWER_SM_SESSION_DATA) + + with patch( + "aioautomower.AutomowerSession", + return_value=AsyncMock( + name="AutomowerMockSession", + model=AutomowerSession, + data=session, + register_data_callback=MagicMock(), + unregister_data_callback=MagicMock(), + register_token_callback=MagicMock(), + connect=AsyncMock(), + ), + ) as automower_session_mock: + automower_coordinator_mock = MagicMock( + name="MockCoordinator", session=automower_session_mock() + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + return config_entry @pytest.mark.asyncio @@ -40,4 +88,4 @@ async def test_load_no_data(hass: HomeAssistant): coordinator = hass.data[DOMAIN]["automower_test"] coordinator.session.data = AUTOMOWER_ERROR_SESSION_DATA with pytest.raises(KeyError): - AutomowerEntity(coordinator, MWR_ONE_IDX) + entity = AutomowerEntity(coordinator, MWR_ONE_IDX) diff --git a/custom_components/husqvarna_automower/tests/test_image.py b/custom_components/husqvarna_automower/tests/test_image.py index 94e63e5..1c7e41f 100644 --- a/custom_components/husqvarna_automower/tests/test_image.py +++ b/custom_components/husqvarna_automower/tests/test_image.py @@ -1,18 +1,22 @@ """Tests for image module.""" import asyncio import io +from datetime import datetime, timedelta from pathlib import Path -from unittest.mock import patch +from unittest.mock import AsyncMock, MagicMock, patch -from PIL import Image +import PIL.Image as Image import pytest +from aioautomower import AutomowerSession from homeassistant.core import HomeAssistant +from pytest_homeassistant_custom_component.common import MockConfigEntry from ..image import AutomowerImage from ..const import CONF_ZONES, DOMAIN, ENABLE_IMAGE from .const import ( - AUTOMER_SM_CONFIG, AUTOMER_DM_CONFIG, + AUTOMOWER_CONFIG_DATA, + AUTOMOWER_DM_SESSION_DATA, MWR_ONE_ID, MWR_ONE_IDX, MWR_TWO_ID, @@ -20,75 +24,88 @@ ENABLE_IMAGE, ) -from .test_common import setup_entity - @pytest.mark.asyncio -async def test_load_image_disabled(hass: HomeAssistant): - """test automower initialization""" +async def setup_image( + hass: HomeAssistant, + mwr_id: int, + mwr_idx: str, + enable_image: bool = True, + replacement_conf_zones: str = "", +): + """Set up image and config entry""" options = AUTOMER_DM_CONFIG.copy() - options[MWR_ONE_ID][ENABLE_IMAGE] = False - options[MWR_TWO_ID][ENABLE_IMAGE] = False - - with patch( - "custom_components.husqvarna_automower.tests.test_common.AUTOMER_DM_CONFIG", - options, - ): - config_entry = await setup_entity(hass, dual_mower=True) - - coordinator = hass.data[DOMAIN]["automower_test"] - register_data_call_count = coordinator.session.register_data_callback.call_count - image_one = AutomowerImage(coordinator, MWR_ONE_IDX, config_entry, hass) - image_two = AutomowerImage(coordinator, MWR_TWO_IDX, config_entry, hass) + if replacement_conf_zones != "": + options[CONF_ZONES] = replacement_conf_zones - assert image_one.options.get(ENABLE_IMAGE) is False - assert image_two.options.get(ENABLE_IMAGE) is False + options[mwr_id][ENABLE_IMAGE] = enable_image - # Disabled image call register_data_callback - assert ( - coordinator.session.register_data_callback.call_count - == register_data_call_count + config_entry = MockConfigEntry( + domain=DOMAIN, + data=AUTOMOWER_CONFIG_DATA, + options=options, + entry_id="automower_test", + title="Automower Test", ) - assert await config_entry.async_unload(hass) - await hass.async_block_till_done() + config_entry.add_to_hass(hass) + + with patch( + "aioautomower.AutomowerSession", + return_value=AsyncMock( + name="AutomowerMockSession", + model=AutomowerSession, + data=AUTOMOWER_DM_SESSION_DATA, + register_data_callback=MagicMock(), + unregister_data_callback=MagicMock(), + ), + ) as automower_session_mock: + automower_coordinator_mock = MagicMock( + name="MockCoordinator", session=automower_session_mock() + ) + + mwr_img = AutomowerImage( + automower_coordinator_mock, mwr_idx, config_entry, hass + ) + return mwr_img, automower_coordinator_mock @pytest.mark.asyncio async def test_load_image_enabled(hass: HomeAssistant): """test automower initialization""" - options = AUTOMER_DM_CONFIG.copy() - options[MWR_ONE_ID][ENABLE_IMAGE] = True - options[MWR_TWO_ID][ENABLE_IMAGE] = True + image_one, automower_coordinator_mock = await setup_image( + hass, MWR_ONE_ID, MWR_ONE_IDX, enable_image=False + ) + image_two, automower_coordinator_mock = await setup_image( + hass, MWR_TWO_ID, MWR_TWO_IDX, enable_image=False + ) - with patch( - "custom_components.husqvarna_automower.tests.test_common.AUTOMER_DM_CONFIG", - options, - ): - config_entry = await setup_entity(hass, dual_mower=True) + assert image_one.options.get(ENABLE_IMAGE) is False - coordinator = hass.data[DOMAIN]["automower_test"] - register_data_call_count = coordinator.session.register_data_callback.call_count + assert image_two.options.get(ENABLE_IMAGE) is False + + # Disabled image call register_data_callback + automower_coordinator_mock.session.register_data_callback.assert_not_called() - image_one = AutomowerImage(coordinator, MWR_ONE_IDX, config_entry, hass) - image_two = AutomowerImage(coordinator, MWR_TWO_IDX, config_entry, hass) + image_one, automower_coordinator_mock = await setup_image( + hass, MWR_ONE_ID, MWR_ONE_IDX + ) + image_two, automower_coordinator_mock = await setup_image( + hass, MWR_TWO_ID, MWR_TWO_IDX + ) assert image_one.options.get(ENABLE_IMAGE) is True + assert image_two.options.get(ENABLE_IMAGE) is True - # Enable image call register_data_callback - assert ( - coordinator.session.register_data_callback.call_count - == register_data_call_count + 2 - ) + # Enabled image call register_data_callback + automower_coordinator_mock.session.register_data_callback.assert_called_once() # Generate Image - # pylint: disable=protected-access await asyncio.to_thread(image_one._generate_image, {}) - # pylint: disable=protected-access await asyncio.to_thread(image_two._generate_image, {}) # Ensure output directory is present @@ -112,47 +129,51 @@ async def test_load_image_enabled(hass: HomeAssistant): assert image.height == 195 # Resize maintains aspect ratio # Mower at home - coordinator.session.data["data"][MWR_ONE_IDX]["attributes"]["mower"][ + automower_coordinator_mock.session.data["data"][MWR_ONE_IDX]["attributes"]["mower"][ "activity" ] = "CHARGING" assert image_one.is_home is True - # pylint: disable=protected-access await asyncio.to_thread(image_one._generate_image, {}) # Mower not at home - coordinator.session.data["data"][MWR_ONE_IDX]["attributes"]["mower"][ + automower_coordinator_mock.session.data["data"][MWR_ONE_IDX]["attributes"]["mower"][ "activity" ] = "MOWING" assert image_one.is_home is False - # pylint: disable=protected-access - # pylint: disable=protected-access await asyncio.to_thread(image_one._generate_image, {}) # Mower, no home location image_one.home_location = None - # pylint: disable=protected-access await asyncio.to_thread(image_one._generate_image, {}) # Single position history exp_result = [ - coordinator.session.data["data"][MWR_ONE_IDX]["attributes"]["positions"][0] - ] + coordinator.session.data["data"][MWR_ONE_IDX]["attributes"]["positions"] - coordinator.session.data["data"][MWR_ONE_IDX]["attributes"]["positions"] = [ - coordinator.session.data["data"][MWR_ONE_IDX]["attributes"]["positions"][0] + automower_coordinator_mock.session.data["data"][MWR_ONE_IDX]["attributes"][ + "positions" + ][0] + ] + automower_coordinator_mock.session.data["data"][MWR_ONE_IDX]["attributes"][ + "positions" + ] + automower_coordinator_mock.session.data["data"][MWR_ONE_IDX]["attributes"][ + "positions" + ] = [ + automower_coordinator_mock.session.data["data"][MWR_ONE_IDX]["attributes"][ + "positions" + ][0] ] - # pylint: disable=protected-access await asyncio.to_thread(image_one._generate_image, {}) assert image_one._position_history[MWR_ONE_ID] == exp_result # Single position history, but it's the first position update exp_result = [ - coordinator.session.data["data"][MWR_ONE_IDX]["attributes"]["positions"][0] + automower_coordinator_mock.session.data["data"][MWR_ONE_IDX]["attributes"][ + "positions" + ][0] ] - coordinator.session.data["data"][MWR_ONE_IDX]["attributes"][ + automower_coordinator_mock.session.data["data"][MWR_ONE_IDX]["attributes"][ "positions" ] = exp_result image_one._position_history = {} - # pylint: disable=protected-access await asyncio.to_thread(image_one._generate_image, {}) assert image_one._position_history[MWR_ONE_ID] == exp_result @@ -160,74 +181,40 @@ async def test_load_image_enabled(hass: HomeAssistant): @pytest.mark.asyncio async def test_load_image_enabled_bad_zone(hass: HomeAssistant): """test automower initialization bad zone, not a dict""" - options = AUTOMER_SM_CONFIG.copy() - options[MWR_ONE_ID][ENABLE_IMAGE] = True - options[CONF_ZONES] = "[]" - - with patch( - "custom_components.husqvarna_automower.tests.test_common.AUTOMER_SM_CONFIG", - options, - ): - config_entry = await setup_entity(hass) - - coordinator = hass.data[DOMAIN]["automower_test"] - AutomowerImage(coordinator, MWR_ONE_IDX, config_entry, hass) + image, automower_coordinator_mock = await setup_image( + hass, MWR_ONE_ID, MWR_ONE_IDX, enable_image=True, replacement_conf_zones="[]" + ) @pytest.mark.asyncio async def test_load_image_enabled_empty_zone(hass: HomeAssistant): """test automower initialization empty zone dict""" - options = AUTOMER_SM_CONFIG.copy() - options[MWR_ONE_ID][ENABLE_IMAGE] = True - options[CONF_ZONES] = "{}" - - with patch( - "custom_components.husqvarna_automower.tests.test_common.AUTOMER_SM_CONFIG", - options, - ): - config_entry = await setup_entity(hass) - - coordinator = hass.data[DOMAIN]["automower_test"] - AutomowerImage(coordinator, MWR_ONE_IDX, config_entry, hass) + image, automower_coordinator_mock = await setup_image( + hass, MWR_ONE_ID, MWR_ONE_IDX, enable_image=True, replacement_conf_zones="{}" + ) @pytest.mark.asyncio async def test_load_image_enabled_zone_without_mower(hass: HomeAssistant): """test automower initialization with a zone, but no mower selected""" - options = AUTOMER_SM_CONFIG.copy() - options[MWR_ONE_ID][ENABLE_IMAGE] = True - options[CONF_ZONES] = ( - '{"front_garden": {"zone_coordinates": [[35.5408367, -82.5524521],' - ' [35.5403893, -82.552613], [35.5399462, -82.5506738]], "sel_mowers": [],' - ' "color": [255, 0, 0], "name": "Front Garden", "display": true}}' + replacement_zones = '{"front_garden": {"zone_coordinates": [[35.5408367, -82.5524521], [35.5403893, -82.552613], [35.5399462, -82.5506738]], "sel_mowers": [], "color": [255, 0, 0], "name": "Front Garden", "display": true}}' + image, automower_coordinator_mock = await setup_image( + hass, + MWR_ONE_ID, + MWR_ONE_IDX, + enable_image=True, + replacement_conf_zones=replacement_zones, ) - with patch( - "custom_components.husqvarna_automower.tests.test_common.AUTOMER_SM_CONFIG", - options, - ): - config_entry = await setup_entity(hass) - - coordinator = hass.data[DOMAIN]["automower_test"] - AutomowerImage(coordinator, MWR_ONE_IDX, config_entry, hass) - @pytest.mark.asyncio async def test_load_image_enabled_zone_no_coordinates(hass: HomeAssistant): """test automower initialization with a zone, but no coordinates in zone""" - options = AUTOMER_SM_CONFIG.copy() - options[MWR_ONE_ID][ENABLE_IMAGE] = True - options[CONF_ZONES] = ( - '{"front_garden": {"zone_coordinates": [], "sel_mowers": ' - '["c7233734-b219-4287-a173-08e3643f89f0"], "color": [255, 0, 0],' - ' "name": "Front Garden", "display": true}}' + replacement_zones = '{"front_garden": {"zone_coordinates": [], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0"], "color": [255, 0, 0], "name": "Front Garden", "display": true}}' + image, automower_coordinator_mock = await setup_image( + hass, + MWR_ONE_ID, + MWR_ONE_IDX, + enable_image=True, + replacement_conf_zones=replacement_zones, ) - - with patch( - "custom_components.husqvarna_automower.tests.test_common.AUTOMER_SM_CONFIG", - options, - ): - config_entry = await setup_entity(hass) - - coordinator = hass.data[DOMAIN]["automower_test"] - AutomowerImage(coordinator, MWR_ONE_IDX, config_entry, hass) diff --git a/custom_components/husqvarna_automower/tests/test_init.py b/custom_components/husqvarna_automower/tests/test_init.py index 205bb4c..a9acff1 100644 --- a/custom_components/husqvarna_automower/tests/test_init.py +++ b/custom_components/husqvarna_automower/tests/test_init.py @@ -1,25 +1,52 @@ """Tests for init module.""" -from asyncio.exceptions import TimeoutError as AsyncioTimeoutError +from asyncio.exceptions import TimeoutError from unittest.mock import AsyncMock, MagicMock, patch import pytest -from aioautomower import GetMowerData, MowerApiConnectionsError - +from aioautomower import AutomowerSession +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers.issue_registry import async_get +from pytest_homeassistant_custom_component.common import MockConfigEntry from .. import async_reload_entry, update_listener from ..const import DOMAIN, MAP_IMG_ROTATION, MAP_PATH_COLOR, HOME_LOCATION from .const import ( + AUTOMER_SM_CONFIG, + AUTOMOWER_CONFIG_DATA, AUTOMOWER_CONFIG_DATA_BAD_SCOPE, AUTOMOWER_SM_SESSION_DATA, + AUTOMOWER_DM_SESSION_DATA, MWR_ONE_ID, MWR_TWO_ID, ) -from .test_common import setup_entity, configure_application_credentials + +async def configure_application_credentials(hass: HomeAssistant): + """Configure application credentials""" + app_cred_config_entry = MockConfigEntry( + domain="application_credentials", + data={}, + entry_id="application_credentials", + title="Application Credentials", + ) + app_cred_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(app_cred_config_entry.entry_id) + + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential( + "test_client_id", + "test_config_secret", + ), + ) @pytest.mark.asyncio @@ -28,9 +55,14 @@ async def test_load_unload(hass: HomeAssistant): await configure_application_credentials(hass) - config_entry = await setup_entity(hass) - assert config_entry.state == ConfigEntryState.LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + config_entry = MockConfigEntry( + domain=DOMAIN, + data=AUTOMOWER_CONFIG_DATA, + options=AUTOMER_SM_CONFIG, + entry_id="automower_test", + title="Automower Test", + ) + config_entry.add_to_hass(hass) with patch( "aioautomower.AutomowerSession", @@ -42,27 +74,28 @@ async def test_load_unload(hass: HomeAssistant): unregister_data_callback=MagicMock(), ), ): - with patch( - "aioautomower.GetMowerData", - return_value=AsyncMock(name="GetMowerMock", model=GetMowerData, data={}), - ): - # Reload entry - No real test, just calling the function - await async_reload_entry(hass, config_entry) - assert config_entry.state == ConfigEntryState.LOADED - - # Update Listner - No real test, just calling the function - await update_listener(hass, config_entry) - assert config_entry.state == ConfigEntryState.LOADED - - assert await config_entry.async_unload(hass) - await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + # Reload entry - No real test, just calling the function + await async_reload_entry(hass, config_entry) + assert config_entry.state == ConfigEntryState.LOADED + + # Update Listner - No real test, just calling the function + await update_listener(hass, config_entry) + assert config_entry.state == ConfigEntryState.LOADED + + assert await config_entry.async_unload(hass) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.NOT_LOADED with patch( "aioautomower.AutomowerSession", return_value=AsyncMock( register_token_callback=MagicMock(), - ws_and_token_session=AsyncMock(side_effect=AsyncioTimeoutError), + connect=AsyncMock(side_effect=TimeoutError), ), ): # Timeout Error @@ -78,42 +111,13 @@ async def test_load_unload(hass: HomeAssistant): "aioautomower.AutomowerSession", return_value=AsyncMock( register_token_callback=MagicMock(), - ws_and_token_session=AsyncMock(side_effect=Exception("Test Exception")), - ), - ): - with patch( - "aioautomower.GetMowerData", - return_value=AsyncMock(name="GetMowerMock", model=GetMowerData, data={}), - ): - # Genric Error - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_ERROR - - assert await config_entry.async_unload(hass) - await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.NOT_LOADED - - with patch( - "aioautomower.AutomowerSession", - return_value=AsyncMock( - register_token_callback=MagicMock(), - ws_and_token_session=AsyncMock(), + connect=AsyncMock(side_effect=Exception("Test Exception")), ), ): - with patch( - "aioautomower.GetMowerData", - return_value=AsyncMock( - name="GetMowerMock", - model=GetMowerData, - async_mower_state=AsyncMock( - side_effect=MowerApiConnectionsError("test") - ), - ), - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert config_entry.state == ConfigEntryState.SETUP_ERROR + # Genric Error + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.SETUP_ERROR @pytest.mark.asyncio @@ -122,11 +126,24 @@ async def test_load_unload_wrong_scope(hass: HomeAssistant): await configure_application_credentials(hass) + config_entry = MockConfigEntry( + domain=DOMAIN, + data=AUTOMOWER_CONFIG_DATA_BAD_SCOPE, + options=AUTOMER_SM_CONFIG, + entry_id="automower_test", + title="Automower Test", + ) + config_entry.add_to_hass(hass) + with patch( - "custom_components.husqvarna_automower.tests.test_common.AUTOMOWER_CONFIG_DATA", - AUTOMOWER_CONFIG_DATA_BAD_SCOPE, + "aioautomower.AutomowerSession", + return_value=AsyncMock(register_token_callback=MagicMock()), ): - config_entry = await setup_entity(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 issue_registry = async_get(hass) issue = issue_registry.async_get_issue(DOMAIN, "wrong_scope") @@ -139,7 +156,7 @@ async def test_load_unload_wrong_scope(hass: HomeAssistant): @pytest.mark.asyncio async def test_async_migrate_entry_2_to_4(hass: HomeAssistant): - """test automower migration from version 2 to 4""" + """test automower migration from version 2 to 3""" await configure_application_credentials(hass) @@ -151,11 +168,37 @@ async def test_async_migrate_entry_2_to_4(hass: HomeAssistant): "map_img_path": "custom_components/husqvarna_automower/tests/resources/biltmore-min.png", } + config_entry = MockConfigEntry( + domain=DOMAIN, + data=AUTOMOWER_CONFIG_DATA, + options=old_options_fmt, + entry_id="automower_test", + title="Automower Test", + version=2, + ) + config_entry.add_to_hass(hass) + with patch( - "custom_components.husqvarna_automower.tests.test_common.AUTOMER_DM_CONFIG", - old_options_fmt, - ): - config_entry = await setup_entity(hass, dual_mower=True, conf_version=2) + "aioautomower.AutomowerSession", + return_value=AsyncMock( + name="AutomowerMockSession", + model=AutomowerSession, + data=AUTOMOWER_DM_SESSION_DATA, + register_data_callback=MagicMock(), + unregister_data_callback=MagicMock(), + register_token_callback=MagicMock(), + connect=AsyncMock(), + ), + ) as automower_session_mock: + automower_coordinator_mock = MagicMock( + name="MockCoordinator", session=automower_session_mock() + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert config_entry.version == 4 assert config_entry.options.get("enable_camera") is None @@ -164,9 +207,8 @@ async def test_async_migrate_entry_2_to_4(hass: HomeAssistant): assert config_entry.options.get("mower_img_path") is None assert config_entry.options.get("map_img_path") is None - # pylint: disable=unused-variable for mwr, config in config_entry.options.items(): - for opt_key in old_options_fmt: # pylint: disable=consider-using-dict-items + for opt_key in old_options_fmt: if opt_key != "enable_camera": assert config[opt_key] == old_options_fmt[opt_key] if opt_key == "enable_camera": @@ -188,31 +230,13 @@ async def test_async_migrate_entry_3_to_4(hass: HomeAssistant): await configure_application_credentials(hass) old_options_fmt = { - "configured_zones": '{"front_garden": {"zone_coordinates": [[35.5408367, -82.5524521],' - " [35.5403893, -82.552613], [35.5399462, -82.5506738], [35.5403827, -82.5505236]," - ' [35.5408367, -82.5524521]], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0",' - ' "1c7aec7b-06ff-462e-b307-7c6ae4469047"], "color": [255, 0, 0], "name": "Front Garden",' - ' "display": true}, "west_italian_garden": {"zone_coordinates": [[35.5402452, -82.552951],' - " [35.540075, -82.5530073], [35.5399943, -82.5526425], [35.5401536, -82.5525835]," - ' [35.5402452, -82.552951]], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0"],' - ' "color": [0, 255, 0], "name": "West Italian Garden", "display": true},' - ' "east_italian_garden": {"zone_coordinates": [[35.5398415, -82.5512532],' - " [35.5396822, -82.5513122], [35.5395927, -82.550942], [35.5397498, -82.5508803]," - ' [35.5398415, -82.5512532]], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0"],' - ' "color": [0, 0, 255], "name": "East Italian Garden", "display": true}, "shrub_garden":' - ' {"zone_coordinates": [[35.5397978, -82.5531334], [35.539357, -82.553289],' - " [35.5393198, -82.553128], [35.5394028, -82.5530529], [35.5394443, -82.5529751]," - " [35.5394639, -82.5528866], [35.5394901, -82.5528303], [35.539645, -82.5529242]," - ' [35.5397629, -82.5529698], [35.5397978, -82.5531334]], "sel_mowers": ' - '["c7233734-b219-4287-a173-08e3643f89f0"], "color": [100, 100, 0], "name": "Shrub Garden",' - ' "display": true}}', + "configured_zones": '{"front_garden": {"zone_coordinates": [[35.5408367, -82.5524521], [35.5403893, -82.552613], [35.5399462, -82.5506738], [35.5403827, -82.5505236], [35.5408367, -82.5524521]], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0", "1c7aec7b-06ff-462e-b307-7c6ae4469047"], "color": [255, 0, 0], "name": "Front Garden", "display": true}, "west_italian_garden": {"zone_coordinates": [[35.5402452, -82.552951], [35.540075, -82.5530073], [35.5399943, -82.5526425], [35.5401536, -82.5525835], [35.5402452, -82.552951]], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0"], "color": [0, 255, 0], "name": "West Italian Garden", "display": true}, "east_italian_garden": {"zone_coordinates": [[35.5398415, -82.5512532], [35.5396822, -82.5513122], [35.5395927, -82.550942], [35.5397498, -82.5508803], [35.5398415, -82.5512532]], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0"], "color": [0, 0, 255], "name": "East Italian Garden", "display": true}, "shrub_garden": {"zone_coordinates": [[35.5397978, -82.5531334], [35.539357, -82.553289], [35.5393198, -82.553128], [35.5394028, -82.5530529], [35.5394443, -82.5529751], [35.5394639, -82.5528866], [35.5394901, -82.5528303], [35.539645, -82.5529242], [35.5397629, -82.5529698], [35.5397978, -82.5531334]], "sel_mowers": ["c7233734-b219-4287-a173-08e3643f89f0"], "color": [100, 100, 0], "name": "Shrub Garden", "display": true}}', MWR_ONE_ID: { "enable_camera": True, "gps_top_left": [35.5411008, -82.5527418], "gps_bottom_right": [35.539442, -82.5504646], "mower_img_path": "custom_components/husqvarna_automower/resources/mower.png", - "map_img_path": "custom_components/husqvarna_automower" - "/tests/resources/biltmore-min.png", + "map_img_path": "custom_components/husqvarna_automower/tests/resources/biltmore-min.png", "map_path_color": [255, 0, 0], "map_img_rotation": -16.10, "home_location": [35.54028774, -82.5526962], @@ -223,8 +247,7 @@ async def test_async_migrate_entry_3_to_4(hass: HomeAssistant): "gps_top_left": [35.5411008, -82.5527418], "gps_bottom_right": [35.539442, -82.5504646], "mower_img_path": "custom_components/husqvarna_automower/resources/mower.png", - "map_img_path": "custom_components/husqvarna_automower/" - "tests/resources/biltmore-min.png", + "map_img_path": "custom_components/husqvarna_automower/tests/resources/biltmore-min.png", "map_path_color": [0, 0, 255], "map_img_rotation": -16.10, "home_location": [35.5409924, -82.5525482], @@ -232,11 +255,37 @@ async def test_async_migrate_entry_3_to_4(hass: HomeAssistant): }, } + config_entry = MockConfigEntry( + domain=DOMAIN, + data=AUTOMOWER_CONFIG_DATA, + options=old_options_fmt, + entry_id="automower_test", + title="Automower Test", + version=3, + ) + config_entry.add_to_hass(hass) + with patch( - "custom_components.husqvarna_automower.tests.test_common.AUTOMER_DM_CONFIG", - old_options_fmt, - ): - config_entry = await setup_entity(hass, dual_mower=True, conf_version=3) + "aioautomower.AutomowerSession", + return_value=AsyncMock( + name="AutomowerMockSession", + model=AutomowerSession, + data=AUTOMOWER_DM_SESSION_DATA, + register_data_callback=MagicMock(), + unregister_data_callback=MagicMock(), + register_token_callback=MagicMock(), + connect=AsyncMock(), + ), + ) as automower_session_mock: + automower_coordinator_mock = MagicMock( + name="MockCoordinator", session=automower_session_mock() + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert config_entry.version == 4 diff --git a/custom_components/husqvarna_automower/tests/test_number.py b/custom_components/husqvarna_automower/tests/test_number.py index 37fc93d..191fdaf 100644 --- a/custom_components/husqvarna_automower/tests/test_number.py +++ b/custom_components/husqvarna_automower/tests/test_number.py @@ -1,10 +1,15 @@ """Tests for number module.""" -from unittest.mock import MagicMock +from copy import deepcopy +from unittest.mock import AsyncMock, MagicMock, patch import pytest +from aioautomower import AutomowerSession from aiohttp import ClientResponseError +from dateutil import tz +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import UpdateFailed +from pytest_homeassistant_custom_component.common import MockConfigEntry from ..const import DOMAIN from ..number import ( @@ -13,11 +18,55 @@ AutomowerParkStartNumberEntity, ) from .const import ( + AUTOMER_DM_CONFIG, + AUTOMOWER_CONFIG_DATA, + AUTOMOWER_SM_SESSION_DATA, MWR_ONE_ID, MWR_ONE_IDX, ) -from .test_common import setup_entity + +@pytest.mark.asyncio +async def setup_entity(hass: HomeAssistant): + """Set up entity and config entry""" + + options = deepcopy(AUTOMER_DM_CONFIG) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=AUTOMOWER_CONFIG_DATA, + options=options, + entry_id="automower_test", + title="Automower Test", + ) + + config_entry.add_to_hass(hass) + + session = deepcopy(AUTOMOWER_SM_SESSION_DATA) + + with patch( + "aioautomower.AutomowerSession", + return_value=AsyncMock( + name="AutomowerMockSession", + model=AutomowerSession, + data=session, + register_data_callback=MagicMock(), + unregister_data_callback=MagicMock(), + register_token_callback=MagicMock(), + connect=AsyncMock(), + action=AsyncMock(), + ), + ) as automower_session_mock: + automower_coordinator_mock = MagicMock( + name="MockCoordinator", session=automower_session_mock() + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + return config_entry @pytest.mark.asyncio @@ -27,7 +76,6 @@ async def test_number_cut_height(hass: HomeAssistant): coordinator = hass.data[DOMAIN]["automower_test"] number = AutomowerNumber(coordinator, MWR_ONE_IDX) - # pylint: disable=protected-access assert number._attr_unique_id == f"{MWR_ONE_ID}_cuttingheight" # Success @@ -54,7 +102,6 @@ async def test_number_park_start(hass: HomeAssistant): coordinator, MWR_ONE_IDX, NUMBER_SENSOR_TYPES[0] ) - # pylint: disable=protected-access assert number._attr_unique_id == f"{MWR_ONE_ID}_Park" # Success diff --git a/custom_components/husqvarna_automower/tests/test_select.py b/custom_components/husqvarna_automower/tests/test_select.py index a212ce4..c324b87 100644 --- a/custom_components/husqvarna_automower/tests/test_select.py +++ b/custom_components/husqvarna_automower/tests/test_select.py @@ -1,18 +1,66 @@ """Tests for select module.""" +from copy import deepcopy +from unittest.mock import AsyncMock, MagicMock, patch import pytest +from aioautomower import AutomowerSession +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import UpdateFailed +from pytest_homeassistant_custom_component.common import MockConfigEntry from ..const import DOMAIN, HEADLIGHTMODES from ..select import AutomowerSelect from .const import ( + AUTOMER_DM_CONFIG, + AUTOMOWER_CONFIG_DATA, + AUTOMOWER_SM_SESSION_DATA, MWR_ONE_ID, MWR_ONE_IDX, ) -from .test_common import setup_entity +@pytest.mark.asyncio +async def setup_entity(hass: HomeAssistant): + """Set up entity and config entry""" + + options = deepcopy(AUTOMER_DM_CONFIG) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=AUTOMOWER_CONFIG_DATA, + options=options, + entry_id="automower_test", + title="Automower Test", + ) + + config_entry.add_to_hass(hass) + + session = deepcopy(AUTOMOWER_SM_SESSION_DATA) + + with patch( + "aioautomower.AutomowerSession", + return_value=AsyncMock( + name="AutomowerMockSession", + model=AutomowerSession, + data=session, + register_data_callback=MagicMock(), + unregister_data_callback=MagicMock(), + register_token_callback=MagicMock(), + connect=AsyncMock(), + action=AsyncMock(), + ), + ) as automower_session_mock: + automower_coordinator_mock = MagicMock( + name="MockCoordinator", session=automower_session_mock() + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + return config_entry @pytest.mark.asyncio @@ -22,7 +70,6 @@ async def test_number_cut_height(hass: HomeAssistant): coordinator = hass.data[DOMAIN]["automower_test"] select = AutomowerSelect(coordinator, MWR_ONE_IDX) - # pylint: disable=protected-access assert select._attr_unique_id == f"{MWR_ONE_ID}_headlight_mode" # Not connected @@ -30,14 +77,14 @@ async def test_number_cut_height(hass: HomeAssistant): "connected" ] = False - assert select.available is False + assert select.available == False # Connected coordinator.session.data["data"][MWR_ONE_IDX]["attributes"]["metadata"][ "connected" ] = True - assert select.available is True + assert select.available == True # Current Option assert select.current_option == "evening_only" diff --git a/custom_components/husqvarna_automower/tests/test_sensor.py b/custom_components/husqvarna_automower/tests/test_sensor.py index 601e163..efbe2f0 100644 --- a/custom_components/husqvarna_automower/tests/test_sensor.py +++ b/custom_components/husqvarna_automower/tests/test_sensor.py @@ -1,14 +1,18 @@ """Tests for sensor module.""" from copy import deepcopy -from unittest.mock import patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest +from aioautomower import AutomowerSession +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from pytest_homeassistant_custom_component.common import MockConfigEntry from ..const import DOMAIN, NO_SUPPORT_FOR_CHANGING_CUTTING_HEIGHT, ZONE_ID from ..sensor import SENSOR_TYPES, AutomowerZoneSensor, get_problem from .const import ( - AUTOMER_SM_CONFIG, + AUTOMER_DM_CONFIG, + AUTOMOWER_CONFIG_DATA, AUTOMOWER_SM_SESSION_DATA, DEFAULT_ZONES, FRONT_GARDEN_PNT, @@ -16,24 +20,72 @@ MWR_ONE_IDX, NO_ZONE_PNT, ) -from .test_common import setup_entity + + +@pytest.mark.asyncio +async def setup_zone_sensor( + hass: HomeAssistant, zone_overide: str = None, enable_cut: bool = True +): + """Set up sensor and config entry""" + + options = deepcopy(AUTOMER_DM_CONFIG) + + if zone_overide: + options["configured_zones"] = zone_overide + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=AUTOMOWER_CONFIG_DATA, + options=options, + entry_id="automower_test", + title="Automower Test", + ) + + config_entry.add_to_hass(hass) + + session = deepcopy(AUTOMOWER_SM_SESSION_DATA) + + if not enable_cut: + session["data"][MWR_ONE_IDX]["attributes"]["system"][ + "model" + ] = NO_SUPPORT_FOR_CHANGING_CUTTING_HEIGHT[0] + with patch( + "aioautomower.AutomowerSession", + return_value=AsyncMock( + name="AutomowerMockSession", + model=AutomowerSession, + data=session, + register_data_callback=MagicMock(), + unregister_data_callback=MagicMock(), + register_token_callback=MagicMock(), + connect=AsyncMock(), + ), + ) as automower_session_mock: + automower_coordinator_mock = MagicMock( + name="MockCoordinator", session=automower_session_mock() + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + return config_entry @pytest.mark.asyncio async def test_zone_sensor(hass: HomeAssistant): """test zone.""" - config_entry = await setup_entity(hass, dual_mower=True) + config_entry = await setup_zone_sensor(hass) coordinator = hass.data[DOMAIN]["automower_test"] zone_sensor = AutomowerZoneSensor(coordinator, MWR_ONE_IDX, config_entry) - # pylint: disable=protected-access assert zone_sensor._attr_unique_id == f"{MWR_ONE_ID}_zone_sensor" - # Load zones works - # pylint: disable=protected-access + # # Load zones works assert zone_sensor.zones == DEFAULT_ZONES - # Mower is home + # # Mower is home coordinator.session.data["data"][MWR_ONE_IDX]["attributes"]["mower"][ "activity" ] = "PARKED_IN_CS" @@ -72,53 +124,35 @@ async def test_zone_sensor(hass: HomeAssistant): @pytest.mark.asyncio async def test_zone_sensor_bad_json(hass: HomeAssistant): """test zone sensor if zones aren't a dict""" - options = deepcopy(AUTOMER_SM_CONFIG) - options["configured_zones"] = "[]" - with patch( - "custom_components.husqvarna_automower.tests.test_common.AUTOMER_SM_CONFIG", - options, - ): - config_entry = await setup_entity(hass) - coordinator = hass.data[DOMAIN]["automower_test"] - zone_sensor = AutomowerZoneSensor(coordinator, MWR_ONE_IDX, config_entry) - - # Zone JSON isn't a dict - # pylint: disable=protected-access - zone_sensor._load_zones() + config_entry = await setup_zone_sensor(hass, zone_overide="[]") + coordinator = hass.data[DOMAIN]["automower_test"] + zone_sensor = AutomowerZoneSensor(coordinator, MWR_ONE_IDX, config_entry) - # pylint: disable=use-implicit-booleaness-not-comparison - assert zone_sensor.zones == {} + # Zone JSON isn't a dict + zone_sensor._load_zones() + assert zone_sensor.zones == {} @pytest.mark.asyncio async def test_sensors_no_cut(hass: HomeAssistant): """test sensors if cutting height is missing.""" - session = deepcopy(AUTOMOWER_SM_SESSION_DATA) - session["data"][MWR_ONE_IDX]["attributes"]["system"][ - "model" - ] = NO_SUPPORT_FOR_CHANGING_CUTTING_HEIGHT[0] - - with patch( - "custom_components.husqvarna_automower.tests.test_common.AUTOMOWER_SM_SESSION_DATA", - session, - ): - await setup_entity(hass) + config_entry = await setup_zone_sensor(hass, enable_cut=False) @pytest.mark.asyncio async def test_statistics_sensors(hass: HomeAssistant): """test statistics sensors.""" - test_sensor_types = deepcopy(SENSOR_TYPES) - for sensor in test_sensor_types: - sensor.entity_registry_enabled_default = True + TEST_SENSOR_TYPES = deepcopy(SENSOR_TYPES) + for s in TEST_SENSOR_TYPES: + s.entity_registry_enabled_default = True with patch( - "custom_components.husqvarna_automower.sensor.SENSOR_TYPES", test_sensor_types + "custom_components.husqvarna_automower.sensor.SENSOR_TYPES", TEST_SENSOR_TYPES ): - await setup_entity(hass) + config_entry = await setup_zone_sensor(hass) @pytest.mark.asyncio -async def test_get_problem(hass: HomeAssistant): # pylint: disable=unused-argument +async def test_get_problem(hass: HomeAssistant): """Test get_problem function.""" mower_attributes = { "mower": { diff --git a/custom_components/husqvarna_automower/tests/test_vacuum.py b/custom_components/husqvarna_automower/tests/test_vacuum.py index 56539e3..aab6761 100644 --- a/custom_components/husqvarna_automower/tests/test_vacuum.py +++ b/custom_components/husqvarna_automower/tests/test_vacuum.py @@ -1,9 +1,12 @@ -"""Tests for vacuum module.""" +"""Tests for sensor module.""" +from copy import deepcopy from datetime import datetime from unittest.mock import AsyncMock, MagicMock, patch import pytest +from aioautomower import AutomowerSession from aiohttp import ClientResponseError +from dateutil import tz from homeassistant.components.vacuum import ( STATE_CLEANING, STATE_DOCKED, @@ -12,19 +15,64 @@ STATE_PAUSED, STATE_RETURNING, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConditionErrorMessage, HomeAssistantError from homeassistant.helpers.storage import Store +from pytest_homeassistant_custom_component.common import MockConfigEntry from ..const import DOMAIN from ..vacuum import HusqvarnaAutomowerEntity from .const import ( + AUTOMER_DM_CONFIG, + AUTOMOWER_CONFIG_DATA, + AUTOMOWER_SM_SESSION_DATA, MWR_ONE_ID, MWR_ONE_IDX, ) -from .test_common import setup_entity +@pytest.mark.asyncio +async def setup_entity(hass: HomeAssistant): + """Set up entity and config entry""" + + options = deepcopy(AUTOMER_DM_CONFIG) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=AUTOMOWER_CONFIG_DATA, + options=options, + entry_id="automower_test", + title="Automower Test", + ) + + config_entry.add_to_hass(hass) + + session = deepcopy(AUTOMOWER_SM_SESSION_DATA) + + with patch( + "aioautomower.AutomowerSession", + return_value=AsyncMock( + name="AutomowerMockSession", + model=AutomowerSession, + data=session, + register_data_callback=MagicMock(), + unregister_data_callback=MagicMock(), + register_token_callback=MagicMock(), + connect=AsyncMock(), + action=AsyncMock(), + ), + ) as automower_session_mock: + automower_coordinator_mock = MagicMock( + name="MockCoordinator", session=automower_session_mock() + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + return config_entry @pytest.mark.asyncio @@ -34,7 +82,6 @@ async def test_vacuum_extra_state_attributes(hass: HomeAssistant): coordinator = hass.data[DOMAIN]["automower_test"] vacuum = HusqvarnaAutomowerEntity(coordinator, MWR_ONE_IDX) - # pylint: disable=protected-access assert vacuum._attr_unique_id == MWR_ONE_ID assert vacuum.extra_state_attributes == {"action": None} @@ -64,7 +111,6 @@ def set_activity(activity: str): "activity" ] = activity - # pylint: disable=protected-access assert vacuum._attr_unique_id == MWR_ONE_ID set_activity("") @@ -156,7 +202,6 @@ def set_error_code(code: str): "errorCode" ] = code - # pylint: disable=protected-access assert vacuum._attr_unique_id == MWR_ONE_ID set_state("STOPPED_IN_GARDEN") @@ -171,7 +216,6 @@ async def test_vacuum_commands(hass: HomeAssistant): await setup_entity(hass) coordinator = hass.data[DOMAIN]["automower_test"] vacuum = HusqvarnaAutomowerEntity(coordinator, MWR_ONE_IDX) - # pylint: disable=protected-access assert vacuum._attr_unique_id == MWR_ONE_ID # Start @@ -265,12 +309,7 @@ async def test_vacuum_commands(hass: HomeAssistant): False, False, ) - exp_payload = ( - '{"data": {"type": "calendar", "attributes": {"tasks":' - ' [{"start": 600, "duration": 60, "monday": true, "tuesday": false,' - ' "wednesday": false, "thursday": false, "friday": false,' - ' "saturday": false, "sunday": false}]}}}' - ) + exp_payload = '{"data": {"type": "calendar", "attributes": {"tasks": [{"start": 600, "duration": 60, "monday": true, "tuesday": false, "wednesday": false, "thursday": false, "friday": false, "saturday": false, "sunday": false}]}}}' coordinator.session.action.assert_awaited_once_with( MWR_ONE_ID, exp_payload, "calendar" ) @@ -315,7 +354,6 @@ async def test_vacuum_schedule_selector_success(hass: HomeAssistant): await setup_entity(hass) coordinator = hass.data[DOMAIN]["automower_test"] vacuum = HusqvarnaAutomowerEntity(coordinator, MWR_ONE_IDX) - # pylint: disable=protected-access assert vacuum._attr_unique_id == MWR_ONE_ID mock_async_load = AsyncMock( @@ -342,31 +380,11 @@ async def test_vacuum_schedule_selector_success(hass: HomeAssistant): with patch( "custom_components.husqvarna_automower.vacuum.Store", MagicMock(name="store mock", spec=Store, return_value=storage_mock), - ): + ) as store_mock: await vacuum.async_schedule_selector("schedule.test_schedule") coordinator.session.action.assert_called_once_with( "c7233734-b219-4287-a173-08e3643f89f0", - '{"data": {"type": "calendar", "attributes": {"tasks": ' - '[{"start": 1020, "duration": 240, "monday": true, ' - '"tuesday": false, "wednesday": false, "thursday": false,' - ' "friday": false, "saturday": false, "sunday": false},' - ' {"start": 1020, "duration": 240, "monday": false, "tuesday": true,' - ' "wednesday": false, "thursday": false, "friday": false, ' - '"saturday": false, "sunday": false}, {"start": 1020, "duration": 240,' - ' "monday": false, "tuesday": false, "wednesday": true,' - ' "thursday": false, "friday": false, "saturday": false,' - ' "sunday": false}, {"start": 1020, "duration": 240, "monday": false,' - ' "tuesday": false, "wednesday": false, "thursday": true, "friday": false,' - ' "saturday": false, "sunday": false}, {"start": 1020, "duration": 360,' - ' "monday": false, "tuesday": false, "wednesday": false, "thursday": false,' - ' "friday": true, "saturday": false, "sunday": false}, {"start": 420, ' - '"duration": 180, "monday": false, "tuesday": false, "wednesday": false,' - ' "thursday": false, "friday": false, "saturday": true, "sunday": false},' - ' {"start": 960, "duration": 420, "monday": false, "tuesday": false, ' - '"wednesday": false, "thursday": false, "friday": false, "saturday": true,' - ' "sunday": false}, {"start": 420, "duration": 840, "monday": false, ' - '"tuesday": false, "wednesday": false, "thursday": false, "friday": false, ' - '"saturday": false, "sunday": true}]}}}', + '{"data": {"type": "calendar", "attributes": {"tasks": [{"start": 1020, "duration": 240, "monday": true, "tuesday": false, "wednesday": false, "thursday": false, "friday": false, "saturday": false, "sunday": false}, {"start": 1020, "duration": 240, "monday": false, "tuesday": true, "wednesday": false, "thursday": false, "friday": false, "saturday": false, "sunday": false}, {"start": 1020, "duration": 240, "monday": false, "tuesday": false, "wednesday": true, "thursday": false, "friday": false, "saturday": false, "sunday": false}, {"start": 1020, "duration": 240, "monday": false, "tuesday": false, "wednesday": false, "thursday": true, "friday": false, "saturday": false, "sunday": false}, {"start": 1020, "duration": 360, "monday": false, "tuesday": false, "wednesday": false, "thursday": false, "friday": true, "saturday": false, "sunday": false}, {"start": 420, "duration": 180, "monday": false, "tuesday": false, "wednesday": false, "thursday": false, "friday": false, "saturday": true, "sunday": false}, {"start": 960, "duration": 420, "monday": false, "tuesday": false, "wednesday": false, "thursday": false, "friday": false, "saturday": true, "sunday": false}, {"start": 420, "duration": 840, "monday": false, "tuesday": false, "wednesday": false, "thursday": false, "friday": false, "saturday": false, "sunday": true}]}}}', "calendar", ) @@ -377,8 +395,6 @@ async def test_vacuum_schedule_selector_fail(hass: HomeAssistant): await setup_entity(hass) coordinator = hass.data[DOMAIN]["automower_test"] vacuum = HusqvarnaAutomowerEntity(coordinator, MWR_ONE_IDX) - - # pylint: disable=protected-access assert vacuum._attr_unique_id == MWR_ONE_ID mock_async_load = AsyncMock( @@ -405,7 +421,7 @@ async def test_vacuum_schedule_selector_fail(hass: HomeAssistant): with patch( "custom_components.husqvarna_automower.vacuum.Store", MagicMock(name="store mock", spec=Store, return_value=storage_mock), - ): + ) as store_mock: # Raises ClientResponseError coordinator.session.action.reset_mock() coordinator.session.action.side_effect = ClientResponseError( @@ -416,24 +432,6 @@ async def test_vacuum_schedule_selector_fail(hass: HomeAssistant): await vacuum.async_schedule_selector("schedule.test_schedule") coordinator.session.action.assert_called_once_with( "c7233734-b219-4287-a173-08e3643f89f0", - '{"data": {"type": "calendar", "attributes": {"tasks": [{"start": 1020,' - ' "duration": 240, "monday": true, "tuesday": false, "wednesday": false,' - ' "thursday": false, "friday": false, "saturday": false, "sunday": false},' - ' {"start": 1020, "duration": 240, "monday": false, "tuesday": true, ' - '"wednesday": false, "thursday": false, "friday": false, "saturday": false,' - ' "sunday": false}, {"start": 1020, "duration": 240, "monday": false,' - ' "tuesday": false, "wednesday": true, "thursday": false, "friday": false,' - ' "saturday": false, "sunday": false}, {"start": 1020, "duration": 240,' - ' "monday": false, "tuesday": false, "wednesday": false, "thursday": true,' - ' "friday": false, "saturday": false, "sunday": false}, {"start": 1020,' - ' "duration": 360, "monday": false, "tuesday": false, "wednesday": false,' - ' "thursday": false, "friday": true, "saturday": false, "sunday": false},' - ' {"start": 420, "duration": 180, "monday": false, "tuesday": false,' - ' "wednesday": false, "thursday": false, "friday": false, "saturday": true,' - ' "sunday": false}, {"start": 960, "duration": 420, "monday": false,' - ' "tuesday": false, "wednesday": false, "thursday": false, "friday": false,' - ' "saturday": true, "sunday": false}, {"start": 420, "duration": 840, ' - '"monday": false, "tuesday": false, "wednesday": false, "thursday": false,' - ' "friday": false, "saturday": false, "sunday": true}]}}}', + '{"data": {"type": "calendar", "attributes": {"tasks": [{"start": 1020, "duration": 240, "monday": true, "tuesday": false, "wednesday": false, "thursday": false, "friday": false, "saturday": false, "sunday": false}, {"start": 1020, "duration": 240, "monday": false, "tuesday": true, "wednesday": false, "thursday": false, "friday": false, "saturday": false, "sunday": false}, {"start": 1020, "duration": 240, "monday": false, "tuesday": false, "wednesday": true, "thursday": false, "friday": false, "saturday": false, "sunday": false}, {"start": 1020, "duration": 240, "monday": false, "tuesday": false, "wednesday": false, "thursday": true, "friday": false, "saturday": false, "sunday": false}, {"start": 1020, "duration": 360, "monday": false, "tuesday": false, "wednesday": false, "thursday": false, "friday": true, "saturday": false, "sunday": false}, {"start": 420, "duration": 180, "monday": false, "tuesday": false, "wednesday": false, "thursday": false, "friday": false, "saturday": true, "sunday": false}, {"start": 960, "duration": 420, "monday": false, "tuesday": false, "wednesday": false, "thursday": false, "friday": false, "saturday": true, "sunday": false}, {"start": 420, "duration": 840, "monday": false, "tuesday": false, "wednesday": false, "thursday": false, "friday": false, "saturday": false, "sunday": true}]}}}', "calendar", )