From 8d376027bfd52d6b54a153e181f21fbd207903f2 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Thu, 8 Jan 2026 14:53:00 +0100 Subject: [PATCH] Add support for gas meter in Powerfox integration (#158196) Co-authored-by: Joostlek --- homeassistant/components/powerfox/__init__.py | 22 +- .../components/powerfox/coordinator.py | 47 +- .../components/powerfox/diagnostics.py | 21 +- homeassistant/components/powerfox/entity.py | 10 +- .../components/powerfox/quality_scale.yaml | 5 +- homeassistant/components/powerfox/sensor.py | 169 ++++- .../components/powerfox/strings.json | 36 + tests/components/powerfox/conftest.py | 109 ++- .../powerfox/snapshots/test_diagnostics.ambr | 10 + .../powerfox/snapshots/test_sensor.ambr | 645 ++++++++++++++++++ tests/components/powerfox/test_sensor.py | 18 +- 11 files changed, 1033 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/powerfox/__init__.py b/homeassistant/components/powerfox/__init__.py index c2f6830692c..06ede9dc2c2 100644 --- a/homeassistant/components/powerfox/__init__.py +++ b/homeassistant/components/powerfox/__init__.py @@ -11,7 +11,11 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .coordinator import PowerfoxConfigEntry, PowerfoxDataUpdateCoordinator +from .coordinator import ( + PowerfoxConfigEntry, + PowerfoxDataUpdateCoordinator, + PowerfoxReportDataUpdateCoordinator, +) PLATFORMS: list[Platform] = [Platform.SENSOR] @@ -30,12 +34,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: PowerfoxConfigEntry) -> await client.close() raise ConfigEntryNotReady from err - coordinators: list[PowerfoxDataUpdateCoordinator] = [ - PowerfoxDataUpdateCoordinator(hass, entry, client, device) - for device in devices - # Filter out gas meter devices (Powerfox FLOW adapters) as they are not yet supported and cause integration failures - if device.type != DeviceType.GAS_METER - ] + coordinators: list[ + PowerfoxDataUpdateCoordinator | PowerfoxReportDataUpdateCoordinator + ] = [] + for device in devices: + if device.type == DeviceType.GAS_METER: + coordinators.append( + PowerfoxReportDataUpdateCoordinator(hass, entry, client, device) + ) + continue + coordinators.append(PowerfoxDataUpdateCoordinator(hass, entry, client, device)) await asyncio.gather( *[ diff --git a/homeassistant/components/powerfox/coordinator.py b/homeassistant/components/powerfox/coordinator.py index bd76b7cc166..0f00d94bdf0 100644 --- a/homeassistant/components/powerfox/coordinator.py +++ b/homeassistant/components/powerfox/coordinator.py @@ -2,8 +2,11 @@ from __future__ import annotations +from datetime import datetime + from powerfox import ( Device, + DeviceReport, Powerfox, PowerfoxAuthenticationError, PowerfoxConnectionError, @@ -15,14 +18,18 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt as dt_util from .const import DOMAIN, LOGGER, SCAN_INTERVAL -type PowerfoxConfigEntry = ConfigEntry[list[PowerfoxDataUpdateCoordinator]] +type PowerfoxCoordinator = ( + "PowerfoxDataUpdateCoordinator" | "PowerfoxReportDataUpdateCoordinator" +) +type PowerfoxConfigEntry = ConfigEntry[list[PowerfoxCoordinator]] -class PowerfoxDataUpdateCoordinator(DataUpdateCoordinator[Poweropti]): - """Class to manage fetching Powerfox data from the API.""" +class PowerfoxBaseCoordinator[T](DataUpdateCoordinator[T]): + """Base coordinator handling shared Powerfox logic.""" config_entry: PowerfoxConfigEntry @@ -33,7 +40,7 @@ class PowerfoxDataUpdateCoordinator(DataUpdateCoordinator[Poweropti]): client: Powerfox, device: Device, ) -> None: - """Initialize global Powerfox data updater.""" + """Initialize shared Powerfox coordinator.""" super().__init__( hass, LOGGER, @@ -44,11 +51,37 @@ class PowerfoxDataUpdateCoordinator(DataUpdateCoordinator[Poweropti]): self.client = client self.device = device - async def _async_update_data(self) -> Poweropti: - """Fetch data from Powerfox API.""" + async def _async_update_data(self) -> T: + """Fetch data and normalize Powerfox errors.""" try: - return await self.client.device(device_id=self.device.id) + return await self._async_fetch_data() except PowerfoxAuthenticationError as err: raise ConfigEntryAuthFailed(err) from err except (PowerfoxConnectionError, PowerfoxNoDataError) as err: raise UpdateFailed(err) from err + + async def _async_fetch_data(self) -> T: + """Fetch data from the Powerfox API.""" + raise NotImplementedError + + +class PowerfoxDataUpdateCoordinator(PowerfoxBaseCoordinator[Poweropti]): + """Class to manage fetching Powerfox data from the API.""" + + async def _async_fetch_data(self) -> Poweropti: + """Fetch live device data from the Powerfox API.""" + return await self.client.device(device_id=self.device.id) + + +class PowerfoxReportDataUpdateCoordinator(PowerfoxBaseCoordinator[DeviceReport]): + """Coordinator handling report data from the API.""" + + async def _async_fetch_data(self) -> DeviceReport: + """Fetch report data from the Powerfox API.""" + local_now = datetime.now(tz=dt_util.get_time_zone(self.hass.config.time_zone)) + return await self.client.report( + device_id=self.device.id, + year=local_now.year, + month=local_now.month, + day=local_now.day, + ) diff --git a/homeassistant/components/powerfox/diagnostics.py b/homeassistant/components/powerfox/diagnostics.py index 8514e42537e..18d68c68ae6 100644 --- a/homeassistant/components/powerfox/diagnostics.py +++ b/homeassistant/components/powerfox/diagnostics.py @@ -5,18 +5,18 @@ from __future__ import annotations from datetime import datetime from typing import Any -from powerfox import HeatMeter, PowerMeter, WaterMeter +from powerfox import DeviceReport, HeatMeter, PowerMeter, WaterMeter from homeassistant.core import HomeAssistant -from .coordinator import PowerfoxConfigEntry, PowerfoxDataUpdateCoordinator +from .coordinator import PowerfoxConfigEntry async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: PowerfoxConfigEntry ) -> dict[str, Any]: """Return diagnostics for Powerfox config entry.""" - powerfox_data: list[PowerfoxDataUpdateCoordinator] = entry.runtime_data + powerfox_data = entry.runtime_data return { "devices": [ @@ -68,6 +68,21 @@ async def async_get_config_entry_diagnostics( if isinstance(coordinator.data, HeatMeter) else {} ), + **( + { + "gas_meter": { + "sum": coordinator.data.gas.sum, + "consumption": coordinator.data.gas.consumption, + "consumption_kwh": coordinator.data.gas.consumption_kwh, + "current_consumption": coordinator.data.gas.current_consumption, + "current_consumption_kwh": coordinator.data.gas.current_consumption_kwh, + "sum_currency": coordinator.data.gas.sum_currency, + } + } + if isinstance(coordinator.data, DeviceReport) + and coordinator.data.gas + else {} + ), } for coordinator in powerfox_data ], diff --git a/homeassistant/components/powerfox/entity.py b/homeassistant/components/powerfox/entity.py index 0ab7200ffe8..619a6188b58 100644 --- a/homeassistant/components/powerfox/entity.py +++ b/homeassistant/components/powerfox/entity.py @@ -2,23 +2,27 @@ from __future__ import annotations +from typing import Any + from powerfox import Device from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import PowerfoxDataUpdateCoordinator +from .coordinator import PowerfoxBaseCoordinator -class PowerfoxEntity(CoordinatorEntity[PowerfoxDataUpdateCoordinator]): +class PowerfoxEntity[CoordinatorT: PowerfoxBaseCoordinator[Any]]( + CoordinatorEntity[CoordinatorT] +): """Base entity for Powerfox.""" _attr_has_entity_name = True def __init__( self, - coordinator: PowerfoxDataUpdateCoordinator, + coordinator: CoordinatorT, device: Device, ) -> None: """Initialize Powerfox entity.""" diff --git a/homeassistant/components/powerfox/quality_scale.yaml b/homeassistant/components/powerfox/quality_scale.yaml index f72d25c3684..725a7472120 100644 --- a/homeassistant/components/powerfox/quality_scale.yaml +++ b/homeassistant/components/powerfox/quality_scale.yaml @@ -70,10 +70,7 @@ rules: dynamic-devices: todo entity-category: done entity-device-class: done - entity-disabled-by-default: - status: exempt - comment: | - This integration does not have any entities that should disabled by default. + entity-disabled-by-default: done entity-translations: done exception-translations: done icon-translations: diff --git a/homeassistant/components/powerfox/sensor.py b/homeassistant/components/powerfox/sensor.py index ab60c99a58b..0ba564bd843 100644 --- a/homeassistant/components/powerfox/sensor.py +++ b/homeassistant/components/powerfox/sensor.py @@ -4,8 +4,9 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from typing import TYPE_CHECKING, Any -from powerfox import Device, HeatMeter, PowerMeter, WaterMeter +from powerfox import Device, GasReport, HeatMeter, PowerMeter, WaterMeter from homeassistant.components.sensor import ( SensorDeviceClass, @@ -13,11 +14,16 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.const import UnitOfEnergy, UnitOfPower, UnitOfVolume +from homeassistant.const import CURRENCY_EURO, UnitOfEnergy, UnitOfPower, UnitOfVolume from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from .coordinator import PowerfoxConfigEntry, PowerfoxDataUpdateCoordinator +from .coordinator import ( + PowerfoxBaseCoordinator, + PowerfoxConfigEntry, + PowerfoxDataUpdateCoordinator, + PowerfoxReportDataUpdateCoordinator, +) from .entity import PowerfoxEntity @@ -30,6 +36,13 @@ class PowerfoxSensorEntityDescription[T: (PowerMeter, WaterMeter, HeatMeter)]( value_fn: Callable[[T], float | int | None] +@dataclass(frozen=True, kw_only=True) +class PowerfoxReportSensorEntityDescription(SensorEntityDescription): + """Describes Powerfox report sensor entity.""" + + value_fn: Callable[[GasReport], float | int | None] + + SENSORS_POWER: tuple[PowerfoxSensorEntityDescription[PowerMeter], ...] = ( PowerfoxSensorEntityDescription[PowerMeter]( key="power", @@ -126,6 +139,104 @@ SENSORS_HEAT: tuple[PowerfoxSensorEntityDescription[HeatMeter], ...] = ( ), ) +SENSORS_GAS: tuple[PowerfoxReportSensorEntityDescription, ...] = ( + PowerfoxReportSensorEntityDescription( + key="gas_consumption_today", + translation_key="gas_consumption_today", + native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda gas: gas.sum, + ), + PowerfoxReportSensorEntityDescription( + key="gas_consumption_energy_today", + translation_key="gas_consumption_energy_today", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, + value_fn=lambda gas: gas.consumption_kwh, + ), + PowerfoxReportSensorEntityDescription( + key="gas_current_consumption", + translation_key="gas_current_consumption", + native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, + device_class=SensorDeviceClass.GAS, + value_fn=lambda gas: gas.current_consumption, + ), + PowerfoxReportSensorEntityDescription( + key="gas_current_consumption_energy", + translation_key="gas_current_consumption_energy", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + entity_registry_enabled_default=False, + value_fn=lambda gas: gas.current_consumption_kwh, + ), + PowerfoxReportSensorEntityDescription( + key="gas_cost_today", + translation_key="gas_cost_today", + native_unit_of_measurement=CURRENCY_EURO, + device_class=SensorDeviceClass.MONETARY, + suggested_display_precision=2, + state_class=SensorStateClass.TOTAL, + value_fn=lambda gas: gas.sum_currency, + ), + PowerfoxReportSensorEntityDescription( + key="gas_max_consumption_today", + translation_key="gas_max_consumption_today", + native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, + device_class=SensorDeviceClass.GAS, + value_fn=lambda gas: gas.max_consumption, + ), + PowerfoxReportSensorEntityDescription( + key="gas_min_consumption_today", + translation_key="gas_min_consumption_today", + native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, + device_class=SensorDeviceClass.GAS, + value_fn=lambda gas: gas.min_consumption, + ), + PowerfoxReportSensorEntityDescription( + key="gas_avg_consumption_today", + translation_key="gas_avg_consumption_today", + native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, + device_class=SensorDeviceClass.GAS, + entity_registry_enabled_default=False, + value_fn=lambda gas: gas.avg_consumption, + ), + PowerfoxReportSensorEntityDescription( + key="gas_max_consumption_energy_today", + translation_key="gas_max_consumption_energy_today", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + entity_registry_enabled_default=False, + value_fn=lambda gas: gas.max_consumption_kwh, + ), + PowerfoxReportSensorEntityDescription( + key="gas_min_consumption_energy_today", + translation_key="gas_min_consumption_energy_today", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + entity_registry_enabled_default=False, + value_fn=lambda gas: gas.min_consumption_kwh, + ), + PowerfoxReportSensorEntityDescription( + key="gas_avg_consumption_energy_today", + translation_key="gas_avg_consumption_energy_today", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + entity_registry_enabled_default=False, + value_fn=lambda gas: gas.avg_consumption_kwh, + ), + PowerfoxReportSensorEntityDescription( + key="gas_max_cost_today", + translation_key="gas_max_cost_today", + native_unit_of_measurement=CURRENCY_EURO, + device_class=SensorDeviceClass.MONETARY, + suggested_display_precision=2, + value_fn=lambda gas: gas.max_currency, + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -135,6 +246,20 @@ async def async_setup_entry( """Set up Powerfox sensors based on a config entry.""" entities: list[SensorEntity] = [] for coordinator in entry.runtime_data: + if isinstance(coordinator, PowerfoxReportDataUpdateCoordinator): + gas_report = coordinator.data.gas + if gas_report is None: + continue + entities.extend( + PowerfoxGasSensorEntity( + coordinator=coordinator, + description=description, + device=coordinator.device, + ) + for description in SENSORS_GAS + if description.value_fn(gas_report) is not None + ) + continue if isinstance(coordinator.data, PowerMeter): entities.extend( PowerfoxSensorEntity( @@ -166,23 +291,49 @@ async def async_setup_entry( async_add_entities(entities) -class PowerfoxSensorEntity(PowerfoxEntity, SensorEntity): - """Defines a powerfox power meter sensor.""" +class BasePowerfoxSensorEntity[CoordinatorT: PowerfoxBaseCoordinator[Any]]( + PowerfoxEntity[CoordinatorT], SensorEntity +): + """Common base for Powerfox sensor entities.""" - entity_description: PowerfoxSensorEntityDescription + entity_description: SensorEntityDescription def __init__( self, - coordinator: PowerfoxDataUpdateCoordinator, + coordinator: CoordinatorT, device: Device, - description: PowerfoxSensorEntityDescription, + description: SensorEntityDescription, ) -> None: - """Initialize Powerfox power meter sensor.""" + """Initialize the shared Powerfox sensor.""" super().__init__(coordinator, device) self.entity_description = description self._attr_unique_id = f"{device.id}_{description.key}" + +class PowerfoxSensorEntity(BasePowerfoxSensorEntity[PowerfoxDataUpdateCoordinator]): + """Defines a powerfox poweropti sensor.""" + + coordinator: PowerfoxDataUpdateCoordinator + entity_description: PowerfoxSensorEntityDescription + @property def native_value(self) -> float | int | None: """Return the state of the entity.""" return self.entity_description.value_fn(self.coordinator.data) + + +class PowerfoxGasSensorEntity( + BasePowerfoxSensorEntity[PowerfoxReportDataUpdateCoordinator] +): + """Defines a powerfox gas meter sensor.""" + + coordinator: PowerfoxReportDataUpdateCoordinator + entity_description: PowerfoxReportSensorEntityDescription + + @property + def native_value(self) -> float | int | None: + """Return the state of the entity.""" + gas_report = self.coordinator.data.gas + if TYPE_CHECKING: + assert gas_report is not None + return self.entity_description.value_fn(gas_report) diff --git a/homeassistant/components/powerfox/strings.json b/homeassistant/components/powerfox/strings.json index 6cae3ed4d6a..4d98efa8d15 100644 --- a/homeassistant/components/powerfox/strings.json +++ b/homeassistant/components/powerfox/strings.json @@ -62,6 +62,42 @@ "energy_usage_low_tariff": { "name": "Energy usage low tariff" }, + "gas_avg_consumption_energy_today": { + "name": "Avg gas hourly energy - today" + }, + "gas_avg_consumption_today": { + "name": "Avg gas hourly consumption - today" + }, + "gas_consumption_energy_today": { + "name": "Gas consumption energy - today" + }, + "gas_consumption_today": { + "name": "Gas consumption - today" + }, + "gas_cost_today": { + "name": "Gas cost - today" + }, + "gas_current_consumption": { + "name": "Gas consumption - this hour" + }, + "gas_current_consumption_energy": { + "name": "Gas consumption energy - this hour" + }, + "gas_max_consumption_energy_today": { + "name": "Max gas hourly energy - today" + }, + "gas_max_consumption_today": { + "name": "Max gas hourly consumption - today" + }, + "gas_max_cost_today": { + "name": "Max gas hourly cost - today" + }, + "gas_min_consumption_energy_today": { + "name": "Min gas hourly energy - today" + }, + "gas_min_consumption_today": { + "name": "Min gas hourly consumption - today" + }, "heat_delta_energy": { "name": "Delta energy" }, diff --git a/tests/components/powerfox/conftest.py b/tests/components/powerfox/conftest.py index 1d930394254..2f41358e31e 100644 --- a/tests/components/powerfox/conftest.py +++ b/tests/components/powerfox/conftest.py @@ -4,7 +4,15 @@ from collections.abc import Generator from datetime import UTC, datetime from unittest.mock import AsyncMock, patch -from powerfox import Device, DeviceType, HeatMeter, PowerMeter, WaterMeter +from powerfox import ( + Device, + DeviceReport, + DeviceType, + GasReport, + HeatMeter, + PowerMeter, + WaterMeter, +) import pytest from homeassistant.components.powerfox.const import DOMAIN @@ -13,6 +21,64 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from tests.common import MockConfigEntry +def _power_meter() -> PowerMeter: + """Return a mocked power meter reading.""" + return PowerMeter( + outdated=False, + timestamp=datetime(2024, 11, 26, 10, 48, 51, tzinfo=UTC), + power=111, + energy_usage=1111.111, + energy_return=111.111, + energy_usage_high_tariff=111.111, + energy_usage_low_tariff=111.111, + ) + + +def _water_meter() -> WaterMeter: + """Return a mocked water meter reading.""" + return WaterMeter( + outdated=False, + timestamp=datetime(2024, 11, 26, 10, 48, 51, tzinfo=UTC), + cold_water=1111.111, + warm_water=0.0, + ) + + +def _heat_meter() -> HeatMeter: + """Return a mocked heat meter reading.""" + return HeatMeter( + outdated=False, + timestamp=datetime(2024, 11, 26, 10, 48, 51, tzinfo=UTC), + total_energy=1111.111, + delta_energy=111, + total_volume=1111.111, + delta_volume=0.111, + ) + + +def _gas_report() -> DeviceReport: + """Return a mocked gas report.""" + return DeviceReport( + gas=GasReport( + total_delta=100, + sum=10.5, + total_delta_currency=5.0, + current_consumption=0.4, + current_consumption_kwh=4.0, + consumption=10.5, + consumption_kwh=10.5, + max_consumption=1.5, + min_consumption=0.2, + avg_consumption=0.6, + max_consumption_kwh=1.7, + min_consumption_kwh=0.1, + avg_consumption_kwh=0.5, + sum_currency=2.5, + max_currency=0.3, + ) + ) + + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: """Override async_setup_entry.""" @@ -61,32 +127,25 @@ def mock_powerfox_client() -> Generator[AsyncMock]: type=DeviceType.HEAT_METER, name="Heatopti", ), - ] - client.device.side_effect = [ - PowerMeter( - outdated=False, - timestamp=datetime(2024, 11, 26, 10, 48, 51, tzinfo=UTC), - power=111, - energy_usage=1111.111, - energy_return=111.111, - energy_usage_high_tariff=111.111, - energy_usage_low_tariff=111.111, - ), - WaterMeter( - outdated=False, - timestamp=datetime(2024, 11, 26, 10, 48, 51, tzinfo=UTC), - cold_water=1111.111, - warm_water=0.0, - ), - HeatMeter( - outdated=False, - timestamp=datetime(2024, 11, 26, 10, 48, 51, tzinfo=UTC), - total_energy=1111.111, - delta_energy=111, - total_volume=1111.111, - delta_volume=0.111, + Device( + id="9x9x1f12xx6x", + date_added=datetime(2024, 11, 26, 9, 22, 35, tzinfo=UTC), + main_device=False, + bidirectional=False, + type=DeviceType.GAS_METER, + name="Gasopti", ), ] + device_factories = { + "9x9x1f12xx3x": _power_meter, + "9x9x1f12xx4x": _water_meter, + "9x9x1f12xx5x": _heat_meter, + } + + client.device = AsyncMock( + side_effect=lambda *, device_id: device_factories[device_id]() # type: ignore[index] + ) + client.report = AsyncMock(return_value=_gas_report()) yield client diff --git a/tests/components/powerfox/snapshots/test_diagnostics.ambr b/tests/components/powerfox/snapshots/test_diagnostics.ambr index d749a1b5b60..e19fc4f80f6 100644 --- a/tests/components/powerfox/snapshots/test_diagnostics.ambr +++ b/tests/components/powerfox/snapshots/test_diagnostics.ambr @@ -31,6 +31,16 @@ 'total_volume': 1111.111, }), }), + dict({ + 'gas_meter': dict({ + 'consumption': 10.5, + 'consumption_kwh': 10.5, + 'current_consumption': 0.4, + 'current_consumption_kwh': 4.0, + 'sum': 10.5, + 'sum_currency': 2.5, + }), + }), ]), }) # --- diff --git a/tests/components/powerfox/snapshots/test_sensor.ambr b/tests/components/powerfox/snapshots/test_sensor.ambr index 54976dfaa79..c7ebf625227 100644 --- a/tests/components/powerfox/snapshots/test_sensor.ambr +++ b/tests/components/powerfox/snapshots/test_sensor.ambr @@ -1,4 +1,649 @@ # serializer version: 1 +# name: test_all_sensors[sensor.gasopti_avg_gas_hourly_consumption_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gasopti_avg_gas_hourly_consumption_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Avg gas hourly consumption - today', + 'platform': 'powerfox', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'gas_avg_consumption_today', + 'unique_id': '9x9x1f12xx6x_gas_avg_consumption_today', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensors[sensor.gasopti_avg_gas_hourly_consumption_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'gas', + 'friendly_name': 'Gasopti Avg gas hourly consumption - today', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.gasopti_avg_gas_hourly_consumption_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.6', + }) +# --- +# name: test_all_sensors[sensor.gasopti_avg_gas_hourly_energy_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gasopti_avg_gas_hourly_energy_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Avg gas hourly energy - today', + 'platform': 'powerfox', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'gas_avg_consumption_energy_today', + 'unique_id': '9x9x1f12xx6x_gas_avg_consumption_energy_today', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensors[sensor.gasopti_avg_gas_hourly_energy_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Gasopti Avg gas hourly energy - today', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.gasopti_avg_gas_hourly_energy_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.5', + }) +# --- +# name: test_all_sensors[sensor.gasopti_gas_consumption_energy_this_hour-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gasopti_gas_consumption_energy_this_hour', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Gas consumption energy - this hour', + 'platform': 'powerfox', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'gas_current_consumption_energy', + 'unique_id': '9x9x1f12xx6x_gas_current_consumption_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensors[sensor.gasopti_gas_consumption_energy_this_hour-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Gasopti Gas consumption energy - this hour', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.gasopti_gas_consumption_energy_this_hour', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4.0', + }) +# --- +# name: test_all_sensors[sensor.gasopti_gas_consumption_energy_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gasopti_gas_consumption_energy_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Gas consumption energy - today', + 'platform': 'powerfox', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'gas_consumption_energy_today', + 'unique_id': '9x9x1f12xx6x_gas_consumption_energy_today', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensors[sensor.gasopti_gas_consumption_energy_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Gasopti Gas consumption energy - today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.gasopti_gas_consumption_energy_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.5', + }) +# --- +# name: test_all_sensors[sensor.gasopti_gas_consumption_this_hour-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gasopti_gas_consumption_this_hour', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Gas consumption - this hour', + 'platform': 'powerfox', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'gas_current_consumption', + 'unique_id': '9x9x1f12xx6x_gas_current_consumption', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensors[sensor.gasopti_gas_consumption_this_hour-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'gas', + 'friendly_name': 'Gasopti Gas consumption - this hour', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.gasopti_gas_consumption_this_hour', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.4', + }) +# --- +# name: test_all_sensors[sensor.gasopti_gas_consumption_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gasopti_gas_consumption_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Gas consumption - today', + 'platform': 'powerfox', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'gas_consumption_today', + 'unique_id': '9x9x1f12xx6x_gas_consumption_today', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensors[sensor.gasopti_gas_consumption_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'gas', + 'friendly_name': 'Gasopti Gas consumption - today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.gasopti_gas_consumption_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10.5', + }) +# --- +# name: test_all_sensors[sensor.gasopti_gas_cost_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gasopti_gas_cost_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Gas cost - today', + 'platform': 'powerfox', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'gas_cost_today', + 'unique_id': '9x9x1f12xx6x_gas_cost_today', + 'unit_of_measurement': '€', + }) +# --- +# name: test_all_sensors[sensor.gasopti_gas_cost_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'monetary', + 'friendly_name': 'Gasopti Gas cost - today', + 'state_class': , + 'unit_of_measurement': '€', + }), + 'context': , + 'entity_id': 'sensor.gasopti_gas_cost_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.5', + }) +# --- +# name: test_all_sensors[sensor.gasopti_max_gas_hourly_consumption_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gasopti_max_gas_hourly_consumption_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Max gas hourly consumption - today', + 'platform': 'powerfox', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'gas_max_consumption_today', + 'unique_id': '9x9x1f12xx6x_gas_max_consumption_today', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensors[sensor.gasopti_max_gas_hourly_consumption_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'gas', + 'friendly_name': 'Gasopti Max gas hourly consumption - today', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.gasopti_max_gas_hourly_consumption_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.5', + }) +# --- +# name: test_all_sensors[sensor.gasopti_max_gas_hourly_cost_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gasopti_max_gas_hourly_cost_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Max gas hourly cost - today', + 'platform': 'powerfox', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'gas_max_cost_today', + 'unique_id': '9x9x1f12xx6x_gas_max_cost_today', + 'unit_of_measurement': '€', + }) +# --- +# name: test_all_sensors[sensor.gasopti_max_gas_hourly_cost_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'monetary', + 'friendly_name': 'Gasopti Max gas hourly cost - today', + 'unit_of_measurement': '€', + }), + 'context': , + 'entity_id': 'sensor.gasopti_max_gas_hourly_cost_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.3', + }) +# --- +# name: test_all_sensors[sensor.gasopti_max_gas_hourly_energy_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gasopti_max_gas_hourly_energy_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Max gas hourly energy - today', + 'platform': 'powerfox', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'gas_max_consumption_energy_today', + 'unique_id': '9x9x1f12xx6x_gas_max_consumption_energy_today', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensors[sensor.gasopti_max_gas_hourly_energy_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Gasopti Max gas hourly energy - today', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.gasopti_max_gas_hourly_energy_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.7', + }) +# --- +# name: test_all_sensors[sensor.gasopti_min_gas_hourly_consumption_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gasopti_min_gas_hourly_consumption_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Min gas hourly consumption - today', + 'platform': 'powerfox', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'gas_min_consumption_today', + 'unique_id': '9x9x1f12xx6x_gas_min_consumption_today', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensors[sensor.gasopti_min_gas_hourly_consumption_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'gas', + 'friendly_name': 'Gasopti Min gas hourly consumption - today', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.gasopti_min_gas_hourly_consumption_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.2', + }) +# --- +# name: test_all_sensors[sensor.gasopti_min_gas_hourly_energy_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.gasopti_min_gas_hourly_energy_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Min gas hourly energy - today', + 'platform': 'powerfox', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'gas_min_consumption_energy_today', + 'unique_id': '9x9x1f12xx6x_gas_min_consumption_energy_today', + 'unit_of_measurement': , + }) +# --- +# name: test_all_sensors[sensor.gasopti_min_gas_hourly_energy_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Gasopti Min gas hourly energy - today', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.gasopti_min_gas_hourly_energy_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.1', + }) +# --- # name: test_all_sensors[sensor.heatopti_delta_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/powerfox/test_sensor.py b/tests/components/powerfox/test_sensor.py index 2dfc1227d77..459e8c61c1a 100644 --- a/tests/components/powerfox/test_sensor.py +++ b/tests/components/powerfox/test_sensor.py @@ -6,7 +6,8 @@ from datetime import timedelta from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory -from powerfox import PowerfoxConnectionError +from powerfox import DeviceReport, PowerfoxConnectionError +import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.config_entries import ConfigEntryState @@ -19,6 +20,7 @@ from . import setup_integration from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform +@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_all_sensors( hass: HomeAssistant, mock_powerfox_client: AsyncMock, @@ -51,3 +53,17 @@ async def test_update_failed( await hass.async_block_till_done() assert hass.states.get("sensor.poweropti_energy_usage").state == STATE_UNAVAILABLE + + +async def test_skips_gas_sensors_when_report_missing( + hass: HomeAssistant, + mock_powerfox_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test gas sensors are not created when report lacks gas data.""" + mock_powerfox_client.report.return_value = DeviceReport(gas=None) + + with patch("homeassistant.components.powerfox.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("sensor.gasopti_gas_consumption_today") is None