From a381301bf0d05447690f17bd465552db52a00dd9 Mon Sep 17 00:00:00 2001 From: Frank Klitzing Date: Wed, 6 Aug 2025 14:42:19 +0200 Subject: [PATCH 1/6] feat: add new average-sensor --- .../power_group_monitor/sensor.py | 10 +- .../sensors/average_power_sensor.py | 94 +++++++++++++++++++ .../power_group_monitor/translations/de.json | 3 + 3 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 custom_components/power_group_monitor/sensors/average_power_sensor.py diff --git a/custom_components/power_group_monitor/sensor.py b/custom_components/power_group_monitor/sensor.py index 9c49231..2bcd46b 100644 --- a/custom_components/power_group_monitor/sensor.py +++ b/custom_components/power_group_monitor/sensor.py @@ -23,12 +23,14 @@ from .sensors.power_total_sensor import PowerTotalSensor from .sensors.power_peak_total_sensor import PowerPeakTotalSensor from .sensors.power_standby_total_sensor import PowerStandbyTotalSensor + from .sensors.energy_today_sensor import EnergyTodaySensor from .sensors.energy_total_sensor import EnergyTotalSensor - from .sensors.energy_total_all_sensor import EnergyTotalAllSensor from .sensors.energy_today_all_sensor import EnergyTodayAllSensor +from .sensors.average_power_sensor import AveragePowerSensor + from .const import CONF_GROUP_NAME, CONF_GROUP_ENTITIES, CONF_GROUP_STANDBY _LOGGER = logging.getLogger(__name__) @@ -56,6 +58,7 @@ async def async_setup_entry( # pylint: disable=too-many-locals, too-many-statem entity_list = [] energy_total_list = [] energy_today_list = [] + total_standby_threshold = float(0) for group in groups: @@ -79,8 +82,11 @@ async def async_setup_entry( # pylint: disable=too-many-locals, too-many-statem # Energie pro Gruppe gesamt energie_gesamt_gruppe = EnergyTotalSensor(hass, entry, group_name, power_sensor) + # Durchschnittswert + average_power = AveragePowerSensor(entry, group_name, power_sensor) + entity_list.extend([power_sensor, power_peak_sensor, standby_sensor, - energie_heute_gruppe, energie_gesamt_gruppe]) + energie_heute_gruppe, energie_gesamt_gruppe, average_power]) energy_total_list.extend([energie_gesamt_gruppe]) energy_today_list.extend([energie_heute_gruppe]) diff --git a/custom_components/power_group_monitor/sensors/average_power_sensor.py b/custom_components/power_group_monitor/sensors/average_power_sensor.py new file mode 100644 index 0000000..5fb1c6d --- /dev/null +++ b/custom_components/power_group_monitor/sensors/average_power_sensor.py @@ -0,0 +1,94 @@ +import logging +from datetime import timedelta +from homeassistant.components.statistics.sensor import StatisticsSensor +from homeassistant.components.sensor import SensorEntity +from homeassistant.const import UnitOfPower +from homeassistant.config_entries import ConfigEntry +from .power_sensor import PowerSensor +from ..const import DEVICE_INFO, DOMAIN # noqa: TID252 + +_LOGGER = logging.getLogger(__name__) + +class AveragePowerSensor(SensorEntity): + """Durchschnittliche Leistung über 15 Minuten.""" + + _attr_translation_key = "AveragePowerSensor" + _attr_has_entity_name = True + + def __init__(self, entry: ConfigEntry, group_name: str, source: PowerSensor): + self._entry = entry + self._group_name = group_name + self._attr_translation_placeholders = {"index": self._group_name} + self._source = source + self._attr_unique_id = f"{entry.entry_id}_{self._group_name}_avg_power" + self._attr_native_unit_of_measurement = UnitOfPower.WATT + + self._source_entity_id = source.entity_id + self._statistics_sensor = None + + async def async_added_to_hass(self): + """Wird aufgerufen, wenn die Entity zu HA hinzugefügt wird.""" + + source_entity_id = self._source.entity_id + if source_entity_id is None: + _LOGGER.warning("Sensor entity_id is None during avg setup!") + return + + # Statistik-Sensor anlegen + self._statistics_sensor = StatisticsSensor( + hass=self.hass, + name=f"{self.name} - avg", + unique_id=f"{self._entry.entry_id}_{self._group_name}_avg_power_statistic", + state_characteristic="mean", + source_entity_id=source_entity_id, + samples_max_buffer_size=None, + samples_max_age=timedelta(minutes=15), + samples_keep_last=True, + precision=2, + percentile=0 + ) + + # Beide Entities registrieren – AveragePowerSensor ist bereits registriert + if self.platform: # platform ist nur in async_added_to_hass gesetzt + self.platform.async_add_entities([self._statistics_sensor]) + + await super().async_added_to_hass() + + async def async_update(self): + """Wird periodisch aufgerufen, um den Durchschnittswert zu aktualisieren.""" + value = None + + # 1️⃣ Statistik-Sensor State lesen + if self._statistics_sensor and self._statistics_sensor.entity_id: + stats_state = self.hass.states.get(self._statistics_sensor.entity_id) + if stats_state and stats_state.state not in (None, "unknown", "unavailable"): + try: + value = float(stats_state.state) + except ValueError: + _LOGGER.debug( + "Konnte Statistikwert nicht in float umwandeln: %s", + stats_state.state + ) + + # # 2️⃣ Falls kein Statistikwert → Fallback: PowerSensor + # if value is None: + # source_state = self.hass.states.get(self._source.entity_id) + # if source_state and source_state.state not in (None, "unknown", "unavailable"): + # try: + # value = float(source_state.state) + # except ValueError: + # _LOGGER.debug( + # "Konnte PowerSensor-Wert nicht in float umwandeln: %s", + # source_state.state + # ) + + self._attr_native_value = value + + @property + def device_info(self): + """Liefert die Geräteinformationen für diese Sensor-Entity.""" + return { + "identifiers": {(DOMAIN, self._entry.entry_id)}, + "name": self._entry.title, + **DEVICE_INFO, + } diff --git a/custom_components/power_group_monitor/translations/de.json b/custom_components/power_group_monitor/translations/de.json index 3f81453..cf826cc 100644 --- a/custom_components/power_group_monitor/translations/de.json +++ b/custom_components/power_group_monitor/translations/de.json @@ -65,6 +65,9 @@ }, "EnergyTodayAllSensor":{ "name": "Gesamt - Energie heute" + }, + "AveragePowerSensor":{ + "name": "{index} - Durchschnitt" } } } From 9a74d0d9aae006f5d1f5c0cf44db9158e62b8e01 Mon Sep 17 00:00:00 2001 From: Frank Klitzing Date: Wed, 13 Aug 2025 00:18:36 +0200 Subject: [PATCH 2/6] fix: unvisible in ui --- .../power_group_monitor/sensors/average_power_sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/power_group_monitor/sensors/average_power_sensor.py b/custom_components/power_group_monitor/sensors/average_power_sensor.py index 5fb1c6d..b9997df 100644 --- a/custom_components/power_group_monitor/sensors/average_power_sensor.py +++ b/custom_components/power_group_monitor/sensors/average_power_sensor.py @@ -48,9 +48,12 @@ async def async_added_to_hass(self): percentile=0 ) + # Unsichtbar im UI machen + self._statistics_sensor._attr_entity_registry_hidden_by = "integration" + # Beide Entities registrieren – AveragePowerSensor ist bereits registriert if self.platform: # platform ist nur in async_added_to_hass gesetzt - self.platform.async_add_entities([self._statistics_sensor]) + await self.platform.async_add_entities([self._statistics_sensor]) await super().async_added_to_hass() From 48f2ec5053ceb9ebefe2930837894307093df28a Mon Sep 17 00:00:00 2001 From: Frank Klitzing Date: Fri, 3 Oct 2025 23:02:08 +0200 Subject: [PATCH 3/6] fix: crash at average sensor --- .../power_group_monitor/sensor.py | 13 ++- .../sensors/average_power_all_sensor.py | 98 +++++++++++++++++++ .../sensors/average_power_sensor.py | 89 +++++++---------- 3 files changed, 144 insertions(+), 56 deletions(-) create mode 100644 custom_components/power_group_monitor/sensors/average_power_all_sensor.py diff --git a/custom_components/power_group_monitor/sensor.py b/custom_components/power_group_monitor/sensor.py index 20aa3d4..020550a 100644 --- a/custom_components/power_group_monitor/sensor.py +++ b/custom_components/power_group_monitor/sensor.py @@ -31,6 +31,7 @@ from .sensors.energy_today_all_sensor import EnergyTodayAllSensor from .sensors.average_power_sensor import AveragePowerSensor +from .sensors.average_power_all_sensor import AveragePowerAllSensor from .const import ( CONF_GROUP_NAME, @@ -75,6 +76,7 @@ async def async_setup_entry( # pylint: disable=too-many-locals, too-many-statem entity_list = [] energy_total_list = [] energy_today_list = [] + power_average_list = [] total_standby_threshold = float(0) @@ -96,6 +98,9 @@ async def async_setup_entry( # pylint: disable=too-many-locals, too-many-statem standby_threshold=float(standby_threshold), # konfigurierbarer Wert? ) + # Durchschnittswert + average_power = AveragePowerSensor(entry, group_name, power_sensor) + # Energie pro Gruppe heute energie_heute_gruppe = EnergyTodaySensor( hass, entry, group_id, group_name, power_sensor @@ -105,22 +110,20 @@ async def async_setup_entry( # pylint: disable=too-many-locals, too-many-statem hass, entry, group_id, group_name, power_sensor ) - # Durchschnittswert - average_power = AveragePowerSensor(entry, group_name, power_sensor) - entity_list.extend( [ power_sensor, power_peak_sensor, standby_sensor, energie_heute_gruppe, - energie_gesamt_gruppe, + energie_gesamt_gruppe, average_power, ] ) energy_total_list.extend([energie_gesamt_gruppe]) energy_today_list.extend([energie_heute_gruppe]) + power_average_list.extend([average_power]) async_add_entities(entity_list, update_before_add=True) @@ -135,6 +138,7 @@ async def async_setup_entry( # pylint: disable=too-many-locals, too-many-statem all_energy_total = EnergyTotalAllSensor(entry, energy_total_list) all_energy_today = EnergyTodayAllSensor(entry, energy_today_list) + all_power_average = AveragePowerAllSensor(entry, power_average_list) async_add_entities( [ @@ -143,5 +147,6 @@ async def async_setup_entry( # pylint: disable=too-many-locals, too-many-statem power_standby_total_sensor, all_energy_total, all_energy_today, + all_power_average ] ) diff --git a/custom_components/power_group_monitor/sensors/average_power_all_sensor.py b/custom_components/power_group_monitor/sensors/average_power_all_sensor.py new file mode 100644 index 0000000..b453432 --- /dev/null +++ b/custom_components/power_group_monitor/sensors/average_power_all_sensor.py @@ -0,0 +1,98 @@ +"""Sensor für Gesamtenergie pro Gruppe. + +Dieses Modul definiert einen Sensor für Home Assistant, +der die gesamte Energie gruppenübergreifent berechnet. +Dazu werden einfach die Gruppensensoren addiert. +""" +import logging + +from homeassistant.components.sensor import SensorEntity, SensorStateClass +from homeassistant.const import UnitOfPower +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.event import async_track_state_change_event + +from ..const import DEVICE_INFO, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class AveragePowerAllSensor(SensorEntity): + """Addiert alle Gruppen-Gesamtsummen""" + _attr_translation_key = "AveragePowerAllSensor" + _attr_has_entity_name = True + + def __init__(self, entry: ConfigEntry, obj_entities) -> None: + """Initialisiert den Sensor. + + Args: + entry (ConfigEntry): Die Konfigurationseintrag-Instanz für diese Integration. + + """ + self._attr_suggested_display_precision = 3 + self._entry = entry + self._obj_entities = obj_entities + self._entities = [entity.entity_id for entity in self._obj_entities] + self._unsub = None + + self._attr_unique_id = f"{entry.entry_id}_avg_all_power" + # self._attr_device_class = SensorDeviceClass.M + self._attr_state_class = SensorStateClass.MEASUREMENT + self._attr_native_unit_of_measurement = UnitOfPower.WATT + + self.my_icon = "mdi:counter" + self._attr_native_value = None + + async def async_added_to_hass(self): + """Wird beim Hinzufügen zur Home Assistant-Instanz aufgerufen.""" + + for entity in self._obj_entities: + + if entity.entity_id is None: + _LOGGER.warning("Sensor entity_id is None during standby setup!") + return + + self._entities = [entity.entity_id for entity in self._obj_entities] + self._unsub = async_track_state_change_event(self.hass, self._entities, + self._async_state_changed) + + await self._async_update_value() + + # pylint: disable=unused-argument + async def _async_state_changed(self, event): + # Wird aufgerufen, wenn sich eine Entity im Set ändert + await self._async_update_value() + + async def _async_update_value(self): + total = 0.0 + for entity_id in self._entities: + state = self.hass.states.get(entity_id) + if state is None: + continue + try: + value = float(state.state) + + total += value + except ValueError: + continue + self._attr_native_value = round(total, 3) + self.async_write_ha_state() + + @property + def device_info(self): + """Liefert die Geräteinformationen für diese Sensor-Entity. + + Returns: + dict: Ein Dictionary mit Informationen zur Identifikation + des Geräts in Home Assistant, einschließlich: + - identifiers: Eindeutige Identifikatoren (Domain und Entry ID) + - name: Anzeigename des Geräts + - manufacturer: Herstellername + - model: Modellbezeichnung + + """ + + return { + "identifiers": {(DOMAIN, self._entry.entry_id)}, + "name": self._entry.title, + **DEVICE_INFO, + } diff --git a/custom_components/power_group_monitor/sensors/average_power_sensor.py b/custom_components/power_group_monitor/sensors/average_power_sensor.py index b9997df..8259212 100644 --- a/custom_components/power_group_monitor/sensors/average_power_sensor.py +++ b/custom_components/power_group_monitor/sensors/average_power_sensor.py @@ -1,14 +1,19 @@ import logging -from datetime import timedelta -from homeassistant.components.statistics.sensor import StatisticsSensor +from homeassistant.components import recorder +from datetime import timedelta, datetime, UTC from homeassistant.components.sensor import SensorEntity from homeassistant.const import UnitOfPower from homeassistant.config_entries import ConfigEntry +from homeassistant.components.recorder import history +from homeassistant.util import dt as dt_util + + from .power_sensor import PowerSensor from ..const import DEVICE_INFO, DOMAIN # noqa: TID252 _LOGGER = logging.getLogger(__name__) + class AveragePowerSensor(SensorEntity): """Durchschnittliche Leistung über 15 Minuten.""" @@ -22,7 +27,7 @@ def __init__(self, entry: ConfigEntry, group_name: str, source: PowerSensor): self._source = source self._attr_unique_id = f"{entry.entry_id}_{self._group_name}_avg_power" self._attr_native_unit_of_measurement = UnitOfPower.WATT - + self._attr_suggested_display_precision = 3 self._source_entity_id = source.entity_id self._statistics_sensor = None @@ -34,58 +39,38 @@ async def async_added_to_hass(self): _LOGGER.warning("Sensor entity_id is None during avg setup!") return - # Statistik-Sensor anlegen - self._statistics_sensor = StatisticsSensor( - hass=self.hass, - name=f"{self.name} - avg", - unique_id=f"{self._entry.entry_id}_{self._group_name}_avg_power_statistic", - state_characteristic="mean", - source_entity_id=source_entity_id, - samples_max_buffer_size=None, - samples_max_age=timedelta(minutes=15), - samples_keep_last=True, - precision=2, - percentile=0 - ) - - # Unsichtbar im UI machen - self._statistics_sensor._attr_entity_registry_hidden_by = "integration" - - # Beide Entities registrieren – AveragePowerSensor ist bereits registriert - if self.platform: # platform ist nur in async_added_to_hass gesetzt - await self.platform.async_add_entities([self._statistics_sensor]) - await super().async_added_to_hass() async def async_update(self): - """Wird periodisch aufgerufen, um den Durchschnittswert zu aktualisieren.""" - value = None - - # 1️⃣ Statistik-Sensor State lesen - if self._statistics_sensor and self._statistics_sensor.entity_id: - stats_state = self.hass.states.get(self._statistics_sensor.entity_id) - if stats_state and stats_state.state not in (None, "unknown", "unavailable"): - try: - value = float(stats_state.state) - except ValueError: - _LOGGER.debug( - "Konnte Statistikwert nicht in float umwandeln: %s", - stats_state.state - ) - - # # 2️⃣ Falls kein Statistikwert → Fallback: PowerSensor - # if value is None: - # source_state = self.hass.states.get(self._source.entity_id) - # if source_state and source_state.state not in (None, "unknown", "unavailable"): - # try: - # value = float(source_state.state) - # except ValueError: - # _LOGGER.debug( - # "Konnte PowerSensor-Wert nicht in float umwandeln: %s", - # source_state.state - # ) - - self._attr_native_value = value + self._attr_native_value = await self.async_calculate_average() + + async def async_calculate_average(self): + """Berechne den Durchschnitt der letzten 15 Minuten aus der History.""" + now = dt_util.utcnow() + start = now - timedelta(minutes=15) + + def _fetch(): + return recorder.history.state_changes_during_period( + self.hass, + start_time=start, + end_time=now, + entity_id=self._source.entity_id, + no_attributes=True, + ) + + instance = recorder.get_instance(self.hass) + states = await instance.async_add_executor_job(_fetch) + + values = [] + for state in states.get(self._source.entity_id, []): + try: + values.append(float(state.state)) + except (ValueError, TypeError): + continue + + if values: + return sum(values) / len(values) + return None @property def device_info(self): From 2c303143cee75d5131bee07d21df54854ba51b5a Mon Sep 17 00:00:00 2001 From: Frank Klitzing Date: Fri, 3 Oct 2025 23:05:22 +0200 Subject: [PATCH 4/6] fix: linter errors --- .../sensors/average_power_sensor.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/custom_components/power_group_monitor/sensors/average_power_sensor.py b/custom_components/power_group_monitor/sensors/average_power_sensor.py index 8259212..525eaa8 100644 --- a/custom_components/power_group_monitor/sensors/average_power_sensor.py +++ b/custom_components/power_group_monitor/sensors/average_power_sensor.py @@ -1,15 +1,16 @@ +"""Modul definiert einen 15 Minuten Durchschnittsleistungssensor für Home Assistant. +Dazu wird die History der letzten 15 Minuten ausgewertet.""" import logging +from datetime import timedelta + from homeassistant.components import recorder -from datetime import timedelta, datetime, UTC from homeassistant.components.sensor import SensorEntity -from homeassistant.const import UnitOfPower from homeassistant.config_entries import ConfigEntry -from homeassistant.components.recorder import history +from homeassistant.const import UnitOfPower from homeassistant.util import dt as dt_util - -from .power_sensor import PowerSensor from ..const import DEVICE_INFO, DOMAIN # noqa: TID252 +from .power_sensor import PowerSensor _LOGGER = logging.getLogger(__name__) @@ -24,7 +25,7 @@ def __init__(self, entry: ConfigEntry, group_name: str, source: PowerSensor): self._entry = entry self._group_name = group_name self._attr_translation_placeholders = {"index": self._group_name} - self._source = source + self._source = source self._attr_unique_id = f"{entry.entry_id}_{self._group_name}_avg_power" self._attr_native_unit_of_measurement = UnitOfPower.WATT self._attr_suggested_display_precision = 3 @@ -42,6 +43,7 @@ async def async_added_to_hass(self): await super().async_added_to_hass() async def async_update(self): + """Wird aufgerufen, wenn die Entity aktualisiert wird.""" self._attr_native_value = await self.async_calculate_average() async def async_calculate_average(self): From 8391d6769f674e496456e013e77cb2a714cc14f3 Mon Sep 17 00:00:00 2001 From: Frank Klitzing Date: Fri, 3 Oct 2025 23:09:57 +0200 Subject: [PATCH 5/6] fix: translation --- custom_components/power_group_monitor/translations/de.json | 2 +- custom_components/power_group_monitor/translations/en.json | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/custom_components/power_group_monitor/translations/de.json b/custom_components/power_group_monitor/translations/de.json index cf826cc..2038068 100644 --- a/custom_components/power_group_monitor/translations/de.json +++ b/custom_components/power_group_monitor/translations/de.json @@ -67,7 +67,7 @@ "name": "Gesamt - Energie heute" }, "AveragePowerSensor":{ - "name": "{index} - Durchschnitt" + "name": "{index} - 15 Min. Durchschnitt" } } } diff --git a/custom_components/power_group_monitor/translations/en.json b/custom_components/power_group_monitor/translations/en.json index 89b4a89..43ecb46 100644 --- a/custom_components/power_group_monitor/translations/en.json +++ b/custom_components/power_group_monitor/translations/en.json @@ -65,6 +65,9 @@ }, "EnergyTodayAllSensor":{ "name": "Total - Energy today" + }, + "AveragePowerSensor":{ + "name": "{index} - 15 Min. Average" } } } From fa26d1d2b2a24077af94ff63dcbdc457d7990f74 Mon Sep 17 00:00:00 2001 From: Frank Klitzing Date: Fri, 3 Oct 2025 23:10:35 +0200 Subject: [PATCH 6/6] v0.4.0 --- custom_components/power_group_monitor/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/power_group_monitor/manifest.json b/custom_components/power_group_monitor/manifest.json index 155376b..34f71bb 100644 --- a/custom_components/power_group_monitor/manifest.json +++ b/custom_components/power_group_monitor/manifest.json @@ -1,7 +1,7 @@ { "domain": "power_group_monitor", "name": "PowerGroupMonitor", - "version": "0.3.0", + "version": "0.4.0", "documentation": "https://github.com/mephdrac/PowerGroupMonitor", "issue_tracker": "https://github.com/mephdrac/PowerGroupMonitor/issues", "requirements": [],