From e1687404910c4b67fd6f278c530d3f0da6b4b81b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20D=C3=B6rner?= Date: Sun, 11 Feb 2024 19:02:50 +0100 Subject: [PATCH] Eco preset only for time control heating operation mode, made entitiy initialization more robust and less likely to affect other entities on error --- custom_components/mypyllant/binary_sensor.py | 15 ++-- custom_components/mypyllant/calendar.py | 14 ++-- custom_components/mypyllant/climate.py | 10 +-- custom_components/mypyllant/datetime.py | 12 ++-- custom_components/mypyllant/manifest.json | 2 +- custom_components/mypyllant/number.py | 9 ++- custom_components/mypyllant/sensor.py | 75 ++++++++++++-------- custom_components/mypyllant/switch.py | 9 ++- custom_components/mypyllant/utils.py | 55 ++++++++++++++ custom_components/mypyllant/water_heater.py | 5 +- dev-requirements.txt | 2 +- tests/conftest.py | 2 +- tests/test_binary_sensor.py | 41 +++++++++++ tests/test_calendar.py | 39 ++++++++++ tests/test_climate.py | 50 ++++++++++++- tests/test_sensor.py | 37 ++++++++++ tests/test_utils.py | 17 +++++ tests/test_water_heater.py | 54 +++++++++++++- 18 files changed, 387 insertions(+), 61 deletions(-) create mode 100644 tests/test_utils.py diff --git a/custom_components/mypyllant/binary_sensor.py b/custom_components/mypyllant/binary_sensor.py index abcb145..37ffa40 100644 --- a/custom_components/mypyllant/binary_sensor.py +++ b/custom_components/mypyllant/binary_sensor.py @@ -17,6 +17,7 @@ from . import SystemCoordinator from .const import DOMAIN +from .utils import EntityList _LOGGER = logging.getLogger(__name__) @@ -32,14 +33,16 @@ async def async_setup_entry( _LOGGER.warning("No system data, skipping binary sensors") return - sensors: list[BinarySensorEntity] = [] + sensors: EntityList[BinarySensorEntity] = EntityList() for index, system in enumerate(coordinator.data): - sensors.append(ControlError(index, coordinator)) - sensors.append(ControlOnline(index, coordinator)) - sensors.append(FirmwareUpdateRequired(index, coordinator)) - sensors.append(FirmwareUpdateEnabled(index, coordinator)) + sensors.append(lambda: ControlError(index, coordinator)) + sensors.append(lambda: ControlOnline(index, coordinator)) + sensors.append(lambda: FirmwareUpdateRequired(index, coordinator)) + sensors.append(lambda: FirmwareUpdateEnabled(index, coordinator)) for circuit_index, circuit in enumerate(system.circuits): - sensors.append(CircuitIsCoolingAllowed(index, circuit_index, coordinator)) + sensors.append( + lambda: CircuitIsCoolingAllowed(index, circuit_index, coordinator) + ) async_add_entities(sensors) diff --git a/custom_components/mypyllant/calendar.py b/custom_components/mypyllant/calendar.py index d628f75..4a69fa2 100644 --- a/custom_components/mypyllant/calendar.py +++ b/custom_components/mypyllant/calendar.py @@ -27,7 +27,11 @@ from . import SystemCoordinator from .const import DOMAIN, WEEKDAYS_TO_RFC5545, RFC5545_TO_WEEKDAYS -from .utils import ZoneCoordinatorEntity, DomesticHotWaterCoordinatorEntity +from .utils import ( + ZoneCoordinatorEntity, + DomesticHotWaterCoordinatorEntity, + EntityList, +) _LOGGER = logging.getLogger(__name__) @@ -43,12 +47,14 @@ async def async_setup_entry( _LOGGER.warning("No system data, skipping calendar entities") return - sensors: list[CalendarEntity] = [] + sensors: EntityList[CalendarEntity] = EntityList() for index, system in enumerate(coordinator.data): for zone_index, zone in enumerate(system.zones): - sensors.append(ZoneHeatingCalendar(index, zone_index, coordinator)) + sensors.append(lambda: ZoneHeatingCalendar(index, zone_index, coordinator)) for dhw_index, dhw in enumerate(system.domestic_hot_water): - sensors.append(DomesticHotWaterCalendar(index, dhw_index, coordinator)) + sensors.append( + lambda: DomesticHotWaterCalendar(index, dhw_index, coordinator) + ) async_add_entities(sensors) diff --git a/custom_components/mypyllant/climate.py b/custom_components/mypyllant/climate.py index 0a1631b..98d1f15 100644 --- a/custom_components/mypyllant/climate.py +++ b/custom_components/mypyllant/climate.py @@ -49,7 +49,7 @@ VentilationFanStageType, ) -from custom_components.mypyllant.utils import shorten_zone_name +from custom_components.mypyllant.utils import shorten_zone_name, EntityList from . import SystemCoordinator from .const import ( @@ -112,13 +112,13 @@ async def async_setup_entry( _LOGGER.warning("No system data, skipping climate") return - zone_entities: list[ClimateEntity] = [] - ventilation_entities: list[ClimateEntity] = [] + zone_entities: EntityList[ClimateEntity] = EntityList() + ventilation_entities: EntityList[ClimateEntity] = EntityList() for index, system in enumerate(coordinator.data): for zone_index, _ in enumerate(system.zones): zone_entities.append( - ZoneClimate( + lambda: ZoneClimate( index, zone_index, coordinator, @@ -127,7 +127,7 @@ async def async_setup_entry( ) for ventilation_index, _ in enumerate(system.ventilation): ventilation_entities.append( - VentilationClimate( + lambda: VentilationClimate( index, ventilation_index, coordinator, diff --git a/custom_components/mypyllant/datetime.py b/custom_components/mypyllant/datetime.py index da0086e..efa36a2 100644 --- a/custom_components/mypyllant/datetime.py +++ b/custom_components/mypyllant/datetime.py @@ -9,7 +9,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from custom_components.mypyllant import DOMAIN, SystemCoordinator -from custom_components.mypyllant.utils import HolidayEntity +from custom_components.mypyllant.utils import HolidayEntity, EntityList from myPyllant.utils import get_default_holiday_dates _LOGGER = logging.getLogger(__name__) @@ -26,10 +26,14 @@ async def async_setup_entry( _LOGGER.warning("No system data, skipping date time entities") return - sensors = [] + sensors: EntityList[DateTimeEntity] = EntityList() for index, system in enumerate(coordinator.data): - sensors.append(SystemHolidayStartDateTimeEntity(index, coordinator, config)) - sensors.append(SystemHolidayEndDateTimeEntity(index, coordinator, config)) + sensors.append( + lambda: SystemHolidayStartDateTimeEntity(index, coordinator, config) + ) + sensors.append( + lambda: SystemHolidayEndDateTimeEntity(index, coordinator, config) + ) async_add_entities(sensors) diff --git a/custom_components/mypyllant/manifest.json b/custom_components/mypyllant/manifest.json index b5824a3..590a900 100644 --- a/custom_components/mypyllant/manifest.json +++ b/custom_components/mypyllant/manifest.json @@ -10,7 +10,7 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/signalkraft/mypyllant-component/issues", "requirements": [ - "myPyllant==0.7.12" + "myPyllant==0.7.13" ], "version": "v0.7.3" } diff --git a/custom_components/mypyllant/number.py b/custom_components/mypyllant/number.py index bd5368b..42288ec 100644 --- a/custom_components/mypyllant/number.py +++ b/custom_components/mypyllant/number.py @@ -14,6 +14,7 @@ HolidayEntity, SystemCoordinatorEntity, ZoneCoordinatorEntity, + EntityList, ) _LOGGER = logging.getLogger(__name__) @@ -30,12 +31,14 @@ async def async_setup_entry( _LOGGER.warning("No system data, skipping number entities") return - sensors = [] + sensors: EntityList[NumberEntity] = EntityList() for index, system in enumerate(coordinator.data): - sensors.append(SystemHolidayDurationNumber(index, coordinator)) + sensors.append(lambda: SystemHolidayDurationNumber(index, coordinator)) for zone_index, zone in enumerate(system.zones): - sensors.append(ZoneQuickVetoDurationNumber(index, zone_index, coordinator)) + sensors.append( + lambda: ZoneQuickVetoDurationNumber(index, zone_index, coordinator) + ) async_add_entities(sensors) diff --git a/custom_components/mypyllant/sensor.py b/custom_components/mypyllant/sensor.py index e9d5db9..55d80f0 100644 --- a/custom_components/mypyllant/sensor.py +++ b/custom_components/mypyllant/sensor.py @@ -2,7 +2,7 @@ import logging from collections.abc import Mapping -from typing import Any +from typing import Any, Iterable, Sequence from homeassistant.components.sensor import ( SensorDeviceClass, @@ -33,6 +33,7 @@ SystemCoordinatorEntity, DomesticHotWaterCoordinatorEntity, ZoneCoordinatorEntity, + EntityList, ) from myPyllant.utils import prepare_field_value_for_dict @@ -52,7 +53,7 @@ async def create_system_sensors( hass: HomeAssistant, config: ConfigEntry -) -> list[SensorEntity]: +) -> Sequence[SensorEntity]: system_coordinator: SystemCoordinator = hass.data[DOMAIN][config.entry_id][ "system_coordinator" ] @@ -60,33 +61,35 @@ async def create_system_sensors( _LOGGER.warning("No system data, skipping sensors") return [] - sensors: list[SensorEntity] = [] + sensors: EntityList[SensorEntity] = EntityList() _LOGGER.debug("Creating system sensors for %s", system_coordinator.data) for index, system in enumerate(system_coordinator.data): if system.outdoor_temperature is not None: - sensors.append(SystemOutdoorTemperatureSensor(index, system_coordinator)) + sensors.append( + lambda: SystemOutdoorTemperatureSensor(index, system_coordinator) + ) if system.water_pressure is not None: - sensors.append(SystemWaterPressureSensor(index, system_coordinator)) - sensors.append(HomeEntity(index, system_coordinator)) + sensors.append(lambda: SystemWaterPressureSensor(index, system_coordinator)) + sensors.append(lambda: HomeEntity(index, system_coordinator)) for device_index, device in enumerate(system.devices): _LOGGER.debug("Creating SystemDevice sensors for %s", device) if "water_pressure" in device.operational_data: sensors.append( - SystemDeviceWaterPressureSensor( + lambda: SystemDeviceWaterPressureSensor( index, device_index, system_coordinator ) ) if device.operation_time is not None: sensors.append( - SystemDeviceOperationTimeSensor( + lambda: SystemDeviceOperationTimeSensor( index, device_index, system_coordinator ) ) if device.on_off_cycles is not None: sensors.append( - SystemDeviceOnOffCyclesSensor( + lambda: SystemDeviceOnOffCyclesSensor( index, device_index, system_coordinator ) ) @@ -94,47 +97,57 @@ async def create_system_sensors( for zone_index, zone in enumerate(system.zones): _LOGGER.debug("Creating Zone sensors for %s", zone) sensors.append( - ZoneDesiredRoomTemperatureSetpointSensor( + lambda: ZoneDesiredRoomTemperatureSetpointSensor( index, zone_index, system_coordinator ) ) if zone.current_room_temperature is not None: sensors.append( - ZoneCurrentRoomTemperatureSensor( + lambda: ZoneCurrentRoomTemperatureSensor( index, zone_index, system_coordinator ) ) if zone.current_room_humidity is not None: sensors.append( - ZoneHumiditySensor(index, zone_index, system_coordinator) + lambda: ZoneHumiditySensor(index, zone_index, system_coordinator) ) sensors.append( - ZoneHeatingOperatingModeSensor(index, zone_index, system_coordinator) + lambda: ZoneHeatingOperatingModeSensor( + index, zone_index, system_coordinator + ) ) if zone.heating_state is not None: sensors.append( - ZoneHeatingStateSensor(index, zone_index, system_coordinator) + lambda: ZoneHeatingStateSensor( + index, zone_index, system_coordinator + ) ) sensors.append( - ZoneCurrentSpecialFunctionSensor(index, zone_index, system_coordinator) + lambda: ZoneCurrentSpecialFunctionSensor( + index, zone_index, system_coordinator + ) ) for circuit_index, circuit in enumerate(system.circuits): _LOGGER.debug("Creating Circuit sensors for %s", circuit) - sensors.append(CircuitStateSensor(index, circuit_index, system_coordinator)) + sensors.append( + lambda: CircuitStateSensor(index, circuit_index, system_coordinator) + ) if circuit.current_circuit_flow_temperature is not None: sensors.append( - CircuitFlowTemperatureSensor( + lambda: CircuitFlowTemperatureSensor( index, circuit_index, system_coordinator ) ) if circuit.heating_curve is not None: sensors.append( - CircuitHeatingCurveSensor(index, circuit_index, system_coordinator) + lambda: CircuitHeatingCurveSensor( + index, circuit_index, system_coordinator + ) ) if circuit.min_flow_temperature_setpoint is not None: sensors.append( - CircuitMinFlowTemperatureSetpointSensor( + lambda: CircuitMinFlowTemperatureSetpointSensor( index, circuit_index, system_coordinator ) ) @@ -143,20 +156,22 @@ async def create_system_sensors( _LOGGER.debug("Creating Domestic Hot Water sensors for %s", dhw) if dhw.current_dhw_temperature: sensors.append( - DomesticHotWaterTankTemperatureSensor( + lambda: DomesticHotWaterTankTemperatureSensor( index, dhw_index, system_coordinator ) ) sensors.append( - DomesticHotWaterSetPointSensor(index, dhw_index, system_coordinator) + lambda: DomesticHotWaterSetPointSensor( + index, dhw_index, system_coordinator + ) ) sensors.append( - DomesticHotWaterOperationModeSensor( + lambda: DomesticHotWaterOperationModeSensor( index, dhw_index, system_coordinator ) ) sensors.append( - DomesticHotWaterCurrentSpecialFunctionSensor( + lambda: DomesticHotWaterCurrentSpecialFunctionSensor( index, dhw_index, system_coordinator ) ) @@ -165,7 +180,7 @@ async def create_system_sensors( async def create_daily_data_sensors( hass: HomeAssistant, config: ConfigEntry -) -> list[SensorEntity]: +) -> Iterable[SensorEntity]: daily_data_coordinator: DailyDataCoordinator = hass.data[DOMAIN][config.entry_id][ "daily_data_coordinator" ] @@ -176,10 +191,12 @@ async def create_daily_data_sensors( _LOGGER.warning("No daily data, skipping sensors") return [] - sensors: list[SensorEntity] = [] + sensors: EntityList[SensorEntity] = EntityList() for system_id, system_devices in daily_data_coordinator.data.items(): _LOGGER.debug("Creating efficiency sensor for System %s", system_id) - sensors.append(EfficiencySensor(system_id, None, daily_data_coordinator)) + sensors.append( + lambda: EfficiencySensor(system_id, None, daily_data_coordinator) + ) for de_index, devices_data in enumerate(system_devices["devices_data"]): if len(devices_data) == 0: continue @@ -189,13 +206,15 @@ async def create_daily_data_sensors( de_index, ) sensors.append( - EfficiencySensor(system_id, de_index, daily_data_coordinator) + lambda: EfficiencySensor(system_id, de_index, daily_data_coordinator) ) for da_index, _ in enumerate( daily_data_coordinator.data[system_id]["devices_data"][de_index] ): sensors.append( - DataSensor(system_id, de_index, da_index, daily_data_coordinator) + lambda: DataSensor( + system_id, de_index, da_index, daily_data_coordinator + ) ) return sensors diff --git a/custom_components/mypyllant/switch.py b/custom_components/mypyllant/switch.py index a7efbc6..a2c2ae1 100644 --- a/custom_components/mypyllant/switch.py +++ b/custom_components/mypyllant/switch.py @@ -11,6 +11,7 @@ from custom_components.mypyllant.utils import ( HolidayEntity, DomesticHotWaterCoordinatorEntity, + EntityList, ) from myPyllant.utils import get_default_holiday_dates @@ -28,12 +29,14 @@ async def async_setup_entry( _LOGGER.warning("No system data, skipping switch entities") return - sensors = [] + sensors: EntityList[SwitchEntity] = EntityList() for index, system in enumerate(coordinator.data): - sensors.append(SystemHolidaySwitch(index, coordinator, config)) + sensors.append(lambda: SystemHolidaySwitch(index, coordinator, config)) for dhw_index, dhw in enumerate(system.domestic_hot_water): - sensors.append(DomesticHotWaterBoostSwitch(index, dhw_index, coordinator)) + sensors.append( + lambda: DomesticHotWaterBoostSwitch(index, dhw_index, coordinator) + ) async_add_entities(sensors) diff --git a/custom_components/mypyllant/utils.py b/custom_components/mypyllant/utils.py index d76753f..ffed781 100644 --- a/custom_components/mypyllant/utils.py +++ b/custom_components/mypyllant/utils.py @@ -3,7 +3,9 @@ import logging import typing from asyncio.exceptions import CancelledError +from collections.abc import MutableSequence from datetime import datetime, timedelta +from typing import TypeVar from aiohttp.client_exceptions import ClientResponseError from homeassistant.config_entries import ConfigEntry @@ -18,6 +20,59 @@ logger = logging.getLogger(__name__) +_T = TypeVar("_T") + + +class EntityList(MutableSequence[_T]): + """ + A list that takes a callable for the item value, calls it, and logs exceptions without raising them + When adding multiple entities in a setup function, an error on one entity + doesn't prevent the others from being added + """ + + def __init__(self, *args): + self.list = list() + self.extend(list(args)) + + def __len__(self): + return len(self.list) + + def __getitem__(self, i): + return self.list[i] + + def __delitem__(self, i): + del self.list[i] + + def __setitem__(self, i, value): + value, raised = self.call_and_log(value) + if not raised: + self.list[i] = value + else: + del self.list[i] + + def call_and_log(self, value): + if callable(value): + try: + return value(), False + except Exception as e: + logger.error(f"Error initializing entity: {e}", exc_info=e) + return None, True + else: + return value, False + + def append(self, value): + value, raised = self.call_and_log(value) + if not raised: + self.list.append(value) + + def insert(self, i, value): + value, raised = self.call_and_log(value) + if not raised: + self.list.insert(i, self.call_and_log(value)) + + def __str__(self): + return str(self.list) + class SystemCoordinatorEntity(CoordinatorEntity): coordinator: "SystemCoordinator" diff --git a/custom_components/mypyllant/water_heater.py b/custom_components/mypyllant/water_heater.py index 0a6ea64..39cfead 100644 --- a/custom_components/mypyllant/water_heater.py +++ b/custom_components/mypyllant/water_heater.py @@ -33,6 +33,7 @@ SERVICE_SET_DHW_CIRCULATION_TIME_PROGRAM, SERVICE_SET_DHW_TIME_PROGRAM, ) +from .utils import EntityList _LOGGER = logging.getLogger(__name__) @@ -48,11 +49,11 @@ async def async_setup_entry( _LOGGER.warning("No system data, skipping water heater") return - dhws: list[WaterHeaterEntity] = [] + dhws: EntityList[WaterHeaterEntity] = EntityList() for index, system in enumerate(coordinator.data): for dhw_index, dhw in enumerate(system.domestic_hot_water): - dhws.append(DomesticHotWaterEntity(index, dhw_index, coordinator)) + dhws.append(lambda: DomesticHotWaterEntity(index, dhw_index, coordinator)) async_add_entities(dhws) if len(dhws) > 0: diff --git a/dev-requirements.txt b/dev-requirements.txt index c95ec91..712cc5c 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -15,5 +15,5 @@ types-PyYAML~=6.0.12.12 pytest==7.4.3 pytest-cov==4.1.0 pytest-homeassistant-custom-component==0.13.77 -myPyllant==0.7.12 +myPyllant==0.7.13 dacite~=1.7.0 \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index d194ccf..311e08b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -125,7 +125,7 @@ def __init__( ): """Initialize a mock config entry.""" kwargs = { - "entry_id": entry_id or uuid.uuid4(), + "entry_id": entry_id or str(uuid.uuid4()), "domain": domain, "data": data or {}, "pref_disable_new_entities": pref_disable_new_entities, diff --git a/tests/test_binary_sensor.py b/tests/test_binary_sensor.py index 73f6c25..93ee30b 100644 --- a/tests/test_binary_sensor.py +++ b/tests/test_binary_sensor.py @@ -1,4 +1,9 @@ +from unittest.mock import Mock + import pytest +from homeassistant.helpers.entity_registry import DATA_REGISTRY, EntityRegistry +from homeassistant.loader import DATA_COMPONENTS, DATA_INTEGRATIONS + from myPyllant.api import MyPyllantAPI from myPyllant.models import System from myPyllant.tests.utils import list_test_data @@ -9,7 +14,43 @@ ControlError, ControlOnline, SystemControlEntity, + async_setup_entry, ) +from custom_components.mypyllant.const import DOMAIN +from tests.conftest import MockConfigEntry, TEST_OPTIONS +from tests.test_init import test_user_input + + +@pytest.mark.parametrize("test_data", list_test_data()) +async def test_async_setup_binary_sensors( + hass, + mypyllant_aioresponses, + mocked_api: MyPyllantAPI, + system_coordinator_mock, + test_data, +): + hass.data[DATA_COMPONENTS] = {} + hass.data[DATA_INTEGRATIONS] = {} + hass.data[DATA_REGISTRY] = EntityRegistry(hass) + with mypyllant_aioresponses(test_data) as _: + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Mock Title", + data=test_user_input, + options=TEST_OPTIONS, + ) + system_coordinator_mock.data = ( + await system_coordinator_mock._async_update_data() + ) + hass.data[DOMAIN] = { + config_entry.entry_id: {"system_coordinator": system_coordinator_mock} + } + mock = Mock(return_value=None) + await async_setup_entry(hass, config_entry, mock) + mock.assert_called_once() + assert len(mock.call_args.args[0]) > 0 + + await mocked_api.aiohttp_session.close() @pytest.mark.parametrize("test_data", list_test_data()) diff --git a/tests/test_calendar.py b/tests/test_calendar.py index 734a35d..5f5da63 100644 --- a/tests/test_calendar.py +++ b/tests/test_calendar.py @@ -1,14 +1,53 @@ from datetime import datetime, timedelta, timezone +from unittest.mock import Mock import freezegun import pytest +from homeassistant.helpers.entity_registry import DATA_REGISTRY, EntityRegistry +from homeassistant.loader import DATA_COMPONENTS, DATA_INTEGRATIONS + +from custom_components.mypyllant import DOMAIN from myPyllant.api import MyPyllantAPI from myPyllant.tests.utils import list_test_data from custom_components.mypyllant.calendar import ( ZoneHeatingCalendar, DomesticHotWaterCalendar, + async_setup_entry, ) +from tests.conftest import MockConfigEntry, TEST_OPTIONS +from tests.test_init import test_user_input + + +@pytest.mark.parametrize("test_data", list_test_data()) +async def test_async_setup_calendar( + hass, + mypyllant_aioresponses, + mocked_api: MyPyllantAPI, + system_coordinator_mock, + test_data, +): + hass.data[DATA_COMPONENTS] = {} + hass.data[DATA_INTEGRATIONS] = {} + hass.data[DATA_REGISTRY] = EntityRegistry(hass) + with mypyllant_aioresponses(test_data) as _: + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Mock Title", + data=test_user_input, + options=TEST_OPTIONS, + ) + system_coordinator_mock.data = ( + await system_coordinator_mock._async_update_data() + ) + hass.data[DOMAIN] = { + config_entry.entry_id: {"system_coordinator": system_coordinator_mock} + } + mock = Mock(return_value=None) + await async_setup_entry(hass, config_entry, mock) + mock.assert_called_once() + assert len(mock.call_args.args[0]) > 0 + await mocked_api.aiohttp_session.close() @pytest.mark.parametrize("test_data", list_test_data()) diff --git a/tests/test_climate.py b/tests/test_climate.py index 529d19a..4f31096 100644 --- a/tests/test_climate.py +++ b/tests/test_climate.py @@ -1,13 +1,59 @@ +from unittest import mock + import pytest as pytest from homeassistant.components.climate import HVACMode from homeassistant.components.climate.const import FAN_OFF, PRESET_AWAY from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.helpers.entity_registry import DATA_REGISTRY, EntityRegistry +from homeassistant.loader import DATA_COMPONENTS, DATA_INTEGRATIONS + from myPyllant.api import MyPyllantAPI from myPyllant.tests.utils import list_test_data -from custom_components.mypyllant import SystemCoordinator -from custom_components.mypyllant.climate import VentilationClimate, ZoneClimate +from custom_components.mypyllant import SystemCoordinator, DOMAIN +from custom_components.mypyllant.climate import ( + VentilationClimate, + ZoneClimate, + async_setup_entry, +) from tests.utils import get_config_entry +from tests.conftest import MockConfigEntry, TEST_OPTIONS +from tests.test_init import test_user_input + + +@pytest.mark.parametrize("test_data", list_test_data()) +async def test_async_setup_climate( + hass, + mypyllant_aioresponses, + mocked_api: MyPyllantAPI, + system_coordinator_mock, + test_data, +): + hass.data[DATA_COMPONENTS] = {} + hass.data[DATA_INTEGRATIONS] = {} + hass.data[DATA_REGISTRY] = EntityRegistry(hass) + with mypyllant_aioresponses(test_data) as _: + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Mock Title", + data=test_user_input, + options=TEST_OPTIONS, + ) + system_coordinator_mock.data = ( + await system_coordinator_mock._async_update_data() + ) + hass.data[DOMAIN] = { + config_entry.entry_id: {"system_coordinator": system_coordinator_mock} + } + mock_async_register_entity_service = mock.Mock(return_value=None) + async_add_entities_mock = mock.Mock(return_value=None) + with mock.patch( + "homeassistant.helpers.entity_platform.async_get_current_platform", + side_effect=lambda *args, **kwargs: mock_async_register_entity_service, + ): + await async_setup_entry(hass, config_entry, async_add_entities_mock) + async_add_entities_mock.assert_called() + await mocked_api.aiohttp_session.close() @pytest.mark.parametrize("test_data", list_test_data()) diff --git a/tests/test_sensor.py b/tests/test_sensor.py index ebd31cb..a0e4239 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -1,4 +1,7 @@ import pytest as pytest +from homeassistant.helpers.entity_registry import DATA_REGISTRY, EntityRegistry +from homeassistant.loader import DATA_COMPONENTS, DATA_INTEGRATIONS + from myPyllant.api import MyPyllantAPI from myPyllant.models import DeviceData from myPyllant.enums import CircuitState @@ -24,7 +27,41 @@ ZoneHumiditySensor, SystemDeviceOnOffCyclesSensor, SystemDeviceOperationTimeSensor, + create_system_sensors, ) +from custom_components.mypyllant.const import DOMAIN +from tests.conftest import MockConfigEntry, TEST_OPTIONS +from tests.test_init import test_user_input + + +@pytest.mark.parametrize("test_data", list_test_data()) +async def test_create_system_sensors( + hass, + mypyllant_aioresponses, + mocked_api: MyPyllantAPI, + system_coordinator_mock, + test_data, +): + hass.data[DATA_COMPONENTS] = {} + hass.data[DATA_INTEGRATIONS] = {} + hass.data[DATA_REGISTRY] = EntityRegistry(hass) + with mypyllant_aioresponses(test_data) as _: + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Mock Title", + data=test_user_input, + options=TEST_OPTIONS, + ) + system_coordinator_mock.data = ( + await system_coordinator_mock._async_update_data() + ) + hass.data[DOMAIN] = { + config_entry.entry_id: {"system_coordinator": system_coordinator_mock} + } + sensors = await create_system_sensors(hass, config_entry) + assert len(sensors) > 0 + + await mocked_api.aiohttp_session.close() @pytest.mark.parametrize("test_data", list_test_data()) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..4680811 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,17 @@ +import logging + +from custom_components.mypyllant.utils import EntityList + + +async def test_log_on_entity_exception(caplog): + def _raise(): + raise Exception("Init Exception") + + entities = EntityList() + entities.append(lambda: dict(test=1)) + with caplog.at_level(logging.ERROR): + entities.append(_raise) + entities.append([0]) + assert len(entities) == 2 + assert "test" in entities[0] + assert 0 in entities[1] diff --git a/tests/test_water_heater.py b/tests/test_water_heater.py index e332379..df21f1e 100644 --- a/tests/test_water_heater.py +++ b/tests/test_water_heater.py @@ -1,10 +1,62 @@ +from unittest import mock + import pytest from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.helpers.entity_registry import DATA_REGISTRY, EntityRegistry +from homeassistant.loader import DATA_COMPONENTS, DATA_INTEGRATIONS + +from custom_components.mypyllant import DOMAIN from myPyllant.api import MyPyllantAPI from myPyllant.enums import DHWOperationMode from myPyllant.tests.utils import list_test_data -from custom_components.mypyllant.water_heater import DomesticHotWaterEntity +from custom_components.mypyllant.water_heater import ( + DomesticHotWaterEntity, + async_setup_entry, +) +from tests.conftest import MockConfigEntry, TEST_OPTIONS +from tests.test_init import test_user_input + + +@pytest.mark.parametrize("test_data", list_test_data()) +async def test_async_setup_water_heater( + hass, + mypyllant_aioresponses, + mocked_api: MyPyllantAPI, + system_coordinator_mock, + test_data, +): + hass.data[DATA_COMPONENTS] = {} + hass.data[DATA_INTEGRATIONS] = {} + hass.data[DATA_REGISTRY] = EntityRegistry(hass) + with mypyllant_aioresponses(test_data) as _: + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Mock Title", + data=test_user_input, + options=TEST_OPTIONS, + ) + system_coordinator_mock.data = ( + await system_coordinator_mock._async_update_data() + ) + if not system_coordinator_mock.data[0].domestic_hot_water: + await mocked_api.aiohttp_session.close() + pytest.skip( + f"No DHW in system {system_coordinator_mock.data[0]}, skipping water heater tests" + ) + hass.data[DOMAIN] = { + config_entry.entry_id: {"system_coordinator": system_coordinator_mock} + } + async_add_entities_mock = mock.Mock(return_value=None) + mock_async_register_entity_service = mock.Mock(return_value=None) + with mock.patch( + "homeassistant.helpers.entity_platform.async_get_current_platform", + side_effect=lambda *args, **kwargs: mock_async_register_entity_service, + ): + await async_setup_entry(hass, config_entry, async_add_entities_mock) + async_add_entities_mock.assert_called_once() + assert len(async_add_entities_mock.call_args.args[0]) > 0 + await mocked_api.aiohttp_session.close() @pytest.mark.parametrize("test_data", list_test_data())