Add support for gas meter in Powerfox integration (#158196)

Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
Klaas Schoute
2026-01-08 14:53:00 +01:00
committed by GitHub
parent 47e91bc2ec
commit 8d376027bf
11 changed files with 1033 additions and 59 deletions

View File

@@ -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(
*[

View File

@@ -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,
)

View File

@@ -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
],

View File

@@ -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."""

View File

@@ -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:

View File

@@ -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)

View File

@@ -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"
},

View File

@@ -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

View File

@@ -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,
}),
}),
]),
})
# ---

View File

@@ -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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.GAS: 'gas'>,
'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': <UnitOfVolume.CUBIC_METERS: 'm³'>,
})
# ---
# 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': <UnitOfVolume.CUBIC_METERS: 'm³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.gasopti_avg_gas_hourly_consumption_today',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'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': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# 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': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.gasopti_avg_gas_hourly_energy_today',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'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': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# 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': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.gasopti_gas_consumption_energy_this_hour',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '4.0',
})
# ---
# name: test_all_sensors[sensor.gasopti_gas_consumption_energy_today-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'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': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# 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': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.gasopti_gas_consumption_energy_today',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.GAS: 'gas'>,
'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': <UnitOfVolume.CUBIC_METERS: 'm³'>,
})
# ---
# 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': <UnitOfVolume.CUBIC_METERS: 'm³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.gasopti_gas_consumption_this_hour',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.4',
})
# ---
# name: test_all_sensors[sensor.gasopti_gas_consumption_today-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.GAS: 'gas'>,
'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': <UnitOfVolume.CUBIC_METERS: 'm³'>,
})
# ---
# name: test_all_sensors[sensor.gasopti_gas_consumption_today-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'gas',
'friendly_name': 'Gasopti Gas consumption - today',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfVolume.CUBIC_METERS: 'm³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.gasopti_gas_consumption_today',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '10.5',
})
# ---
# name: test_all_sensors[sensor.gasopti_gas_cost_today-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL: 'total'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.MONETARY: 'monetary'>,
'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': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': '€',
}),
'context': <ANY>,
'entity_id': 'sensor.gasopti_gas_cost_today',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.GAS: 'gas'>,
'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': <UnitOfVolume.CUBIC_METERS: 'm³'>,
})
# ---
# 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': <UnitOfVolume.CUBIC_METERS: 'm³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.gasopti_max_gas_hourly_consumption_today',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.MONETARY: 'monetary'>,
'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': <ANY>,
'entity_id': 'sensor.gasopti_max_gas_hourly_cost_today',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'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': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# 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': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.gasopti_max_gas_hourly_energy_today',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.GAS: 'gas'>,
'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': <UnitOfVolume.CUBIC_METERS: 'm³'>,
})
# ---
# 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': <UnitOfVolume.CUBIC_METERS: 'm³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.gasopti_min_gas_hourly_consumption_today',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'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': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'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': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# 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': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.gasopti_min_gas_hourly_energy_today',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.1',
})
# ---
# name: test_all_sensors[sensor.heatopti_delta_energy-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -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