From 6ad9f420ab7e0e85146853fe9fd8ef5c12421ccb Mon Sep 17 00:00:00 2001 From: Vincent Knoop Pathuis <48653141+vpathuis@users.noreply.github.com> Date: Sat, 18 Mar 2023 20:50:50 +0100 Subject: [PATCH] Add Landis+Gyr poll on restart (#89644) --- .../landisgyr_heat_meter/__init__.py | 1 + .../components/landisgyr_heat_meter/sensor.py | 133 +++++++++++------- .../landisgyr_heat_meter/test_sensor.py | 120 +--------------- 3 files changed, 87 insertions(+), 167 deletions(-) diff --git a/homeassistant/components/landisgyr_heat_meter/__init__.py b/homeassistant/components/landisgyr_heat_meter/__init__.py index 541fef017d0..3a44267bd41 100644 --- a/homeassistant/components/landisgyr_heat_meter/__init__.py +++ b/homeassistant/components/landisgyr_heat_meter/__init__.py @@ -29,6 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + await coordinator.async_config_entry_first_refresh() await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/landisgyr_heat_meter/sensor.py b/homeassistant/components/landisgyr_heat_meter/sensor.py index 508ae43b8e3..af966297421 100644 --- a/homeassistant/components/landisgyr_heat_meter/sensor.py +++ b/homeassistant/components/landisgyr_heat_meter/sensor.py @@ -1,14 +1,16 @@ """Platform for sensor integration.""" from __future__ import annotations -from dataclasses import asdict +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime import logging from ultraheat_api.response import HeatMeterResponse from homeassistant.components.sensor import ( - RestoreSensor, SensorDeviceClass, + SensorEntity, SensorEntityDescription, SensorStateClass, ) @@ -25,6 +27,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -36,177 +39,220 @@ from . import DOMAIN _LOGGER = logging.getLogger(__name__) +@dataclass +class HeatMeterSensorEntityDescriptionMixin: + """Mixin for additional Heat Meter sensor description attributes .""" + + value_fn: Callable[[HeatMeterResponse], StateType | datetime] + + +@dataclass +class HeatMeterSensorEntityDescription( + SensorEntityDescription, HeatMeterSensorEntityDescriptionMixin +): + """Heat Meter sensor description.""" + + HEAT_METER_SENSOR_TYPES = ( - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="volume_usage_m3", icon="mdi:fire", name="Volume usage", device_class=SensorDeviceClass.VOLUME, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, state_class=SensorStateClass.TOTAL, + value_fn=lambda res: getattr(res, "volume_usage_m3", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="heat_usage_gj", icon="mdi:fire", name="Heat usage GJ", native_unit_of_measurement=UnitOfEnergy.GIGA_JOULE, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, + value_fn=lambda res: getattr(res, "heat_usage_gj", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="heat_previous_year_gj", icon="mdi:fire", name="Heat previous year GJ", - native_unit_of_measurement="GJ", + native_unit_of_measurement=UnitOfEnergy.GIGA_JOULE, entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "heat_previous_year_gj", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="volume_previous_year_m3", icon="mdi:fire", name="Volume usage previous year", device_class=SensorDeviceClass.VOLUME, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "volume_previous_year_m3", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="ownership_number", name="Ownership number", icon="mdi:identifier", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "ownership_number", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="error_number", name="Error number", icon="mdi:home-alert", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "error_number", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="device_number", name="Device number", icon="mdi:identifier", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "device_number", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="measurement_period_minutes", name="Measurement period minutes", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "measurement_period_minutes", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="power_max_kw", name="Power max", native_unit_of_measurement=UnitOfPower.KILO_WATT, device_class=SensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "power_max_kw", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="power_max_previous_year_kw", name="Power max previous year", native_unit_of_measurement=UnitOfPower.KILO_WATT, device_class=SensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "power_max_previous_year_kw", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="flowrate_max_m3ph", name="Flowrate max", native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, icon="mdi:water-outline", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "flowrate_max_m3ph", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="flowrate_max_previous_year_m3ph", name="Flowrate max previous year", native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, icon="mdi:water-outline", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "flowrate_max_previous_year_m3ph", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="return_temperature_max_c", name="Return temperature max", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "return_temperature_max_c", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="return_temperature_max_previous_year_c", name="Return temperature max previous year", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr( + res, "return_temperature_max_previous_year_c", None + ), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="flow_temperature_max_c", name="Flow temperature max", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "flow_temperature_max_c", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="flow_temperature_max_previous_year_c", name="Flow temperature max previous year", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "flow_temperature_max_previous_year_c", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="operating_hours", name="Operating hours", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.HOURS, entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "operating_hours", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="flow_hours", name="Flow hours", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.HOURS, entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "flow_hours", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="fault_hours", name="Fault hours", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.HOURS, entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "fault_hours", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="fault_hours_previous_year", name="Fault hours previous year", device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.HOURS, entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "fault_hours_previous_year", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="yearly_set_day", name="Yearly set day", icon="mdi:clock-outline", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "yearly_set_day", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="monthly_set_day", name="Monthly set day", icon="mdi:clock-outline", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "monthly_set_day", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="meter_date_time", name="Meter date time", icon="mdi:clock-outline", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: dt_util.as_utc(res.meter_date_time) + if res.meter_date_time + else None, ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="measuring_range_m3ph", name="Measuring range", native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, icon="mdi:water-outline", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "measuring_range_m3ph", None), ), - SensorEntityDescription( + HeatMeterSensorEntityDescription( key="settings_and_firmware", name="Settings and firmware", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda res: getattr(res, "settings_and_firmware", None), ), ) @@ -238,14 +284,17 @@ async def async_setup_entry( class HeatMeterSensor( - CoordinatorEntity[DataUpdateCoordinator[HeatMeterResponse]], RestoreSensor + CoordinatorEntity[DataUpdateCoordinator[HeatMeterResponse]], + SensorEntity, ): """Representation of a Sensor.""" + entity_description: HeatMeterSensorEntityDescription + def __init__( self, coordinator: DataUpdateCoordinator[HeatMeterResponse], - description: SensorEntityDescription, + description: HeatMeterSensorEntityDescription, device: DeviceInfo, ) -> None: """Set up the sensor with the initial values.""" @@ -254,25 +303,9 @@ class HeatMeterSensor( self._attr_unique_id = f"{coordinator.config_entry.data['device_number']}_{description.key}" # type: ignore[union-attr] self._attr_name = f"Heat Meter {description.name}" self.entity_description = description - self._attr_device_info = device - self._attr_should_poll = bool(self.key in ("heat_usage", "heat_previous_year")) - async def async_added_to_hass(self) -> None: - """Call when entity about to be added to hass.""" - await super().async_added_to_hass() - state = await self.async_get_last_sensor_data() - if state: - self._attr_native_value = state.native_value - - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - if self.key in asdict(self.coordinator.data): - if self.device_class == SensorDeviceClass.TIMESTAMP: - self._attr_native_value = dt_util.as_utc( - asdict(self.coordinator.data)[self.key] - ) - else: - self._attr_native_value = asdict(self.coordinator.data)[self.key] - - self.async_write_ha_state() + @property + def native_value(self) -> StateType | datetime: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/tests/components/landisgyr_heat_meter/test_sensor.py b/tests/components/landisgyr_heat_meter/test_sensor.py index 6296fadd116..a37fab65a10 100644 --- a/tests/components/landisgyr_heat_meter/test_sensor.py +++ b/tests/components/landisgyr_heat_meter/test_sensor.py @@ -5,20 +5,15 @@ from unittest.mock import patch import serial -from homeassistant.components.homeassistant import ( - DOMAIN as HA_DOMAIN, - SERVICE_UPDATE_ENTITY, -) +from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN from homeassistant.components.landisgyr_heat_meter.const import DOMAIN, POLLING_INTERVAL from homeassistant.components.sensor import ( - ATTR_LAST_RESET, ATTR_STATE_CLASS, SensorDeviceClass, SensorStateClass, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, - ATTR_ENTITY_ID, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, STATE_UNAVAILABLE, @@ -26,16 +21,12 @@ from homeassistant.const import ( UnitOfEnergy, UnitOfVolume, ) -from homeassistant.core import CoreState, HomeAssistant, State +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.common import ( - MockConfigEntry, - async_fire_time_changed, - mock_restore_cache_with_extra_data, -) +from tests.common import MockConfigEntry, async_fire_time_changed API_HEAT_METER_SERVICE = ( "homeassistant.components.landisgyr_heat_meter.ultraheat_api.HeatMeterService" @@ -80,13 +71,6 @@ async def test_create_sensors( await hass.config_entries.async_setup(mock_entry.entry_id) await async_setup_component(hass, HA_DOMAIN, {}) await hass.async_block_till_done() - await hass.services.async_call( - HA_DOMAIN, - SERVICE_UPDATE_ENTITY, - {ATTR_ENTITY_ID: "sensor.heat_meter_heat_usage_gj"}, - blocking=True, - ) - await hass.async_block_till_done() # check if 26 attributes have been created assert len(hass.states.async_all()) == 25 @@ -121,97 +105,6 @@ async def test_create_sensors( assert entity_registry_entry.entity_category == EntityCategory.DIAGNOSTIC -@patch(API_HEAT_METER_SERVICE) -async def test_restore_state(mock_heat_meter, hass: HomeAssistant) -> None: - """Test sensor restore state.""" - # Home assistant is not running yet - hass.state = CoreState.not_running - last_reset = "2022-07-01T00:00:00.000000+00:00" - mock_restore_cache_with_extra_data( - hass, - [ - ( - State( - "sensor.heat_meter_heat_usage_gj", - "34167", - attributes={ - ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.GIGA_JOULE, - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - }, - ), - { - "native_value": 34167, - "native_unit_of_measurement": UnitOfEnergy.GIGA_JOULE, - "icon": "mdi:fire", - "last_reset": last_reset, - }, - ), - ( - State( - "sensor.heat_meter_volume_usage", - "456", - attributes={ - ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: UnitOfVolume.CUBIC_METERS, - ATTR_STATE_CLASS: SensorStateClass.TOTAL, - }, - ), - { - "native_value": 456, - "native_unit_of_measurement": UnitOfVolume.CUBIC_METERS, - "icon": "mdi:fire", - "last_reset": last_reset, - }, - ), - ( - State( - "sensor.heat_meter_device_number", - "devicenr_789", - attributes={ - ATTR_LAST_RESET: last_reset, - }, - ), - { - "native_value": "devicenr_789", - "native_unit_of_measurement": None, - "last_reset": last_reset, - }, - ), - ], - ) - entry_data = { - "device": "/dev/USB0", - "model": "LUGCUH50", - "device_number": "123456789", - } - - # create and add entry - mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data) - mock_entry.add_to_hass(hass) - - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() - - # restore from cache - state = hass.states.get("sensor.heat_meter_heat_usage_gj") - assert state - assert state.state == "34167" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.GIGA_JOULE - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL - - state = hass.states.get("sensor.heat_meter_volume_usage") - assert state - assert state.state == "456" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.CUBIC_METERS - assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL - - state = hass.states.get("sensor.heat_meter_device_number") - assert state - assert state.state == "devicenr_789" - assert state.attributes.get(ATTR_STATE_CLASS) is None - - @patch(API_HEAT_METER_SERVICE) async def test_exception_on_polling(mock_heat_meter, hass: HomeAssistant) -> None: """Test sensor.""" @@ -237,13 +130,6 @@ async def test_exception_on_polling(mock_heat_meter, hass: HomeAssistant) -> Non await hass.config_entries.async_setup(mock_entry.entry_id) await async_setup_component(hass, HA_DOMAIN, {}) await hass.async_block_till_done() - await hass.services.async_call( - HA_DOMAIN, - SERVICE_UPDATE_ENTITY, - {ATTR_ENTITY_ID: "sensor.heat_meter_heat_usage_gj"}, - blocking=True, - ) - await hass.async_block_till_done() # check if initial setup succeeded state = hass.states.get("sensor.heat_meter_heat_usage_gj")