Energieverbrauchssensoren - Technische Dokumentation¶
Diese Dokumentation beschreibt die technische Implementierung der Energieverbrauchssensoren (elektrisch und thermisch) in der Lambda Heat Pumps Integration.
Übersicht¶
Die Integration bietet zwei Arten von Energieverbrauchssensoren:
- Elektrische Energieverbrauchssensoren: Messen den Stromverbrauch (kWh)
- Thermische Energieverbrauchssensoren: Messen die Wärmeabgabe (kWh)
Beide Typen werden nach Betriebsart (heating, hot_water, cooling, defrost) und Zeitraum (total, daily, monthly, yearly, hourly bei Heizen) aufgeteilt.
Architektur¶
Komponenten¶
┌─────────────────────────────────────────────────────────────┐
│ Coordinator │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ _track_hp_energy_consumption() │ │
│ │ ├─ _track_hp_energy_type_consumption(electrical) │ │
│ │ └─ _track_hp_energy_type_consumption(thermal) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ _increment_energy_consumption() │ │
│ │ _increment_thermal_energy_consumption() │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ utils.py │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ increment_energy_consumption_counter() │ │
│ │ - sensor_type: "electrical" | "thermal" │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ sensor.py │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ LambdaEnergyConsumptionSensor │ │
│ │ - set_energy_value() │ │
│ │ - native_value (berechnet aus period) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Datenfluss¶
- Quellsensoren liefern kumulative Werte. Pro HP werden sie aus der lambda_wp_config.yaml (Abschnitt
energy_consumption_sensors) gelesen; nur wenn nicht konfiguriert, gelten die Lambda-Standard-Sensoren: - Elektrisch:
sensor_entity_idaus Config oder Defaultcompressor_power_consumption_accumulated(Modbus Register 1021) -
Thermisch:
thermal_sensor_entity_idaus Config (optional) oder Defaultcompressor_thermal_energy_output_accumulated(Modbus Register 1022) -
Coordinator berechnet Deltas:
- Liest Quellsensor-Werte
- Berechnet Delta zwischen Updates
-
Erkennt Betriebsmodus-Wechsel (Flankenerkennung)
-
Utils aktualisieren Sensoren:
increment_energy_consumption_counter()aktualisiert alle Perioden-
Unterstützt sowohl elektrische als auch thermische Sensoren
-
Entities speichern Werte:
LambdaEnergyConsumptionSensorspeichert Total-Wert in_energy_value- Berechnet daily/monthly/yearly aus Total-Wert:
native_value = _energy_value - _yesterday_value(bzw. previous_monthly/yearly) - Persistierung: Gespeichert wird der State (= Anzeige-Wert). Bei Daily/Monthly/Yearly muss beim Restore der kumulative Total aus diesem Wert rekonstruiert oder aus dem Attribut
energy_valuegelesen werden (siehe Abschnitt „Negative Daily/Monthly/Yearly-Werte“).
Einheitliches Delta-Verfahren: Elektrische und thermische Sensoren nutzen dasselbe Berechnungsmodell (Delta zum Vortag bzw. Vormonat/Vorjahr): Daily = Total - Yesterday, Monthly = Total - Previous Monthly, Yearly = Total - Previous Yearly. Dieselbe Logik in LambdaEnergyConsumptionSensor und increment_energy_consumption_counter gilt für beide.
Implementierung¶
1. Sensor-Erstellung¶
Sensoren werden in sensor.py erstellt:
# Elektrische Sensoren
for hp_idx in range(1, num_hps + 1):
for mode in ENERGY_CONSUMPTION_MODES:
for period in ENERGY_CONSUMPTION_PERIODS:
sensor_id = f"{mode}_energy_{period}"
sensor = LambdaEnergyConsumptionSensor(...)
# Thermische Sensoren
for hp_idx in range(1, num_hps + 1):
for mode in ENERGY_CONSUMPTION_MODES:
for period in ENERGY_CONSUMPTION_PERIODS:
sensor_id = f"{mode}_thermal_energy_{period}"
if sensor_template.get("data_type") == "thermal_calculated":
sensor = LambdaEnergyConsumptionSensor(...)
2. Sensor-Templates¶
Templates sind in const.py definiert:
ENERGY_CONSUMPTION_SENSOR_TEMPLATES = {
# Elektrische Sensoren
"heating_energy_total": {
"name": "Heating Energy Total",
"unit": "kWh",
"data_type": "energy_calculated",
"state_class": "total_increasing",
"device_class": "energy",
"operating_state": "heating",
"period": "total",
},
# Thermische Sensoren
"heating_thermal_energy_total": {
"name": "Heating Thermal Energy Total",
"unit": "kWh",
"data_type": "thermal_calculated",
"state_class": "total_increasing",
"device_class": "energy",
"operating_state": "heating",
"period": "total",
},
# ...
}
3. Coordinator-Tracking¶
Der Coordinator trackt beide Energiearten parallel:
async def _track_hp_energy_consumption(self, hp_idx, current_state, data):
# Elektrische Energie
await self._track_hp_energy_type_consumption(
hp_idx, current_state, data,
sensor_type="electrical",
default_sensor_id_template="sensor.{name_prefix}_hp{hp_idx}_compressor_power_consumption_accumulated",
increment_fn=self._increment_energy_consumption
)
# Thermische Energie
await self._track_hp_energy_type_consumption(
hp_idx, current_state, data,
sensor_type="thermal",
default_sensor_id_template="sensor.{name_prefix}_hp{hp_idx}_compressor_thermal_energy_output_accumulated",
increment_fn=self._increment_thermal_energy_consumption
)
4. Delta-Berechnung¶
Die generische Tracking-Funktion:
async def _track_hp_energy_type_consumption(
self, hp_idx, current_state, data, sensor_type,
default_sensor_id_template, unit_check_fn, convert_to_kwh_fn,
last_reading_dict, first_value_seen_dict, increment_fn
):
# 1. Lese Quellsensor
current_energy_state = self.hass.states.get(sensor_entity_id)
current_energy = float(current_energy_state.state)
# 2. Konvertiere zu kWh
current_energy_kwh = convert_to_kwh_fn(current_energy, unit)
# 3. Berechne Delta
last_energy = last_reading_dict.get(f"hp{hp_idx}", None)
energy_delta = calculate_energy_delta(current_energy_kwh, last_energy, max_delta=100.0)
# 4. Bestimme Betriebsmodus
mode = mode_mapping[current_state] # heating, hot_water, cooling, defrost
# 5. Aktualisiere Sensoren bei Moduswechsel oder kontinuierlich
if current_state != last_state or (mode == "stby" or energy_delta > 0):
await increment_fn(hp_idx, mode, energy_delta)
5. Sensor-Update¶
Die Update-Funktion aktualisiert alle Perioden:
async def increment_energy_consumption_counter(
hass, mode, hp_index, energy_delta, name_prefix,
use_legacy_modbus_names=True, energy_offsets=None,
sensor_type="electrical" # oder "thermal"
):
for period in ["total", "daily", "monthly", "yearly", "2h", "4h"]:
# Bestimme sensor_id basierend auf sensor_type
if sensor_type == "thermal":
sensor_id = f"{mode}_thermal_energy_{period}"
else:
sensor_id = f"{mode}_energy_{period}"
# Finde Entity
energy_entity = find_energy_entity(hass, entity_id)
# Berechne neuen Wert
current_value = float(state_obj.state)
new_value = current_value + energy_delta
# Wende Offset an (nur für total)
if period == "total" and energy_offsets:
new_value += offset
# Aktualisiere Entity
energy_entity.set_energy_value(new_value)
6. Entity-Implementierung¶
Die LambdaEnergyConsumptionSensor Klasse:
class LambdaEnergyConsumptionSensor(RestoreEntity, SensorEntity):
def __init__(self, hass, entry, sensor_id, name, entity_id,
unique_id, unit, state_class, device_class,
device_type, hp_index, mode, period):
self._energy_value = 0.0 # Total-Wert
self._period = period
# ...
@property
def native_value(self):
"""Berechnet Wert basierend auf period."""
if self._period == "total":
return self._energy_value
elif self._period == "daily":
return self._energy_value - self._yesterday_value
elif self._period == "monthly":
return self._energy_value - self._previous_monthly_value
# ...
def set_energy_value(self, value):
"""Wird von increment_energy_consumption_counter aufgerufen."""
self._energy_value = value
self.async_write_ha_state()
Quellsensoren¶
Die tatsächlich verwendeten Quellsensoren werden aus der lambda_wp_config.yaml ermittelt (Abschnitt energy_consumption_sensors: pro Wärmepumpe sensor_entity_id (elektrisch) und optional thermal_sensor_entity_id (thermisch). Nur wenn dort nichts konfiguriert ist oder ein Sensor ungültig, werden die folgenden Standard-Quellsensoren (Lambda-Modbus) verwendet.
Elektrische Energie (Standard bei fehlender Konfiguration)¶
- Sensor:
compressor_power_consumption_accumulated - Register: 1021 (HP1), 2021 (HP2), etc.
- Einheit: Wh (wird zu kWh konvertiert)
- Typ: int32, total_increasing
Thermische Energie (Standard bei fehlender Konfiguration)¶
- Sensor:
compressor_thermal_energy_output_accumulated - Register: 1022 (HP1), 2022 (HP2), etc.
- Einheit: Wh (wird zu kWh konvertiert)
- Typ: int32, total_increasing
Die thermischen Energy-Sensoren kommen mit diesem Release hinzu; die elektrischen gab es bereits. Weil die Quellsensoren der COP-Sensoren damit zu unterschiedlichen Zeitpunkten in der Integration vorhanden sind, nutzen die COP-Sensoren eine Baseline (Stichtag), damit die COP nur aus Deltas ab „beide Quellen vorhanden“ berechnet wird. Das gilt für Total- und für alle zyklischen COP-Sensoren (täglich, monatlich, jährlich, stündlich). Siehe COP-Sensoren – Warum Baseline?.
Betriebsmodus-Mapping¶
mode_mapping = {
0: "stby", # Standby
1: "heating", # CH - Heizen
2: "hot_water", # DHW - Warmwasser
3: "cooling", # CC - Kühlen
4: "stby", # Standby (alternativ)
5: "defrost", # DEFROST - Abtauen
}
Flankenerkennung¶
Die Integration nutzt Flankenerkennung, um Energie-Deltas exakt dem aktiven Betriebsmodus zuzuordnen:
- Moduswechsel erkannt: Delta wird dem neuen Modus zugeordnet
- Kontinuierlicher Betrieb: Delta wird dem aktuellen Modus zugeordnet
- Standby: Delta wird auch Standby zugeordnet (falls vorhanden)
Einheitenkonvertierung¶
Unterstützte Einheiten werden automatisch zu kWh konvertiert:
def _convert_energy_to_kwh_cached(self, value, unit):
if unit == "Wh":
return value / 1000.0
elif unit == "kWh":
return value
elif unit == "MWh":
return value * 1000.0
else:
return value # Fallback
Persistierung¶
- Total-Werte: Werden in
LambdaEnergyConsumptionSensorgespeichert (RestoreEntity) - Last Readings: Werden im Coordinator gespeichert (
_last_energy_reading,_last_thermal_energy_reading) - JSON-Persistierung: Coordinator speichert Werte in
cycle_energy_persist.json - Energy-Sensor-States: Zusätzlich zu HA-Restore werden die Energy-Sensor-States (Total, Daily, Monthly, Yearly für elektrisch und thermisch) in
cycle_energy_persist.jsonunter dem Schlüsselenergy_sensor_statesgespeichert; beim Neustart hat diese Quelle Vorrang, damit Anzeigewerte nicht fallen (z. B. 0,44 → 0,4).
Neustart-Werterhalt¶
set_energy_value()verringert nie: Der gespeicherte Wert wird nicht verringert (vermeidet Überschreiben durch veraltete Coordinator-/Total-Werte nach Neustart).- Kein Fallback-
async_set: Kann der Coordinator die Entity-Referenz nicht auflösen, wird keinasync_setmit möglicherweise veraltetem State ausgeführt. native_valueauf 2 Dezimalstellen gerundet: Vermeidet Float-Artefakte im persistierten State (z. B. 0,39999… statt 0,44).- State aus
cycle_energy_persistbevorzugt: Nachrestore_state(last_state)wird, falls der Coordinator einen State auscycle_energy_persistfür diese Entity hat, dieser angewendet (_apply_persisted_energy_state).
Konfiguration¶
Externe Quellsensoren (lambda_wp_config.yaml)¶
Die tatsächlich verwendeten Quellsensoren werden hier definiert. Pro HP können elektrischer und thermischer Quellsensor getrennt konfiguriert werden:
energy_consumption_sensors:
hp1:
sensor_entity_id: "sensor.shelly_lambda_gesamt_leistung" # elektrisch
thermal_sensor_entity_id: "sensor.waermemesser_hp1" # optional, thermisch
sensor_entity_id: Quellsensor für elektrische Energie. Fehlt er (oder ist ungültig), wirdsensor.{name}_hp{n}_compressor_power_consumption_accumulatedverwendet.thermal_sensor_entity_id(optional): Quellsensor für thermische Energie. Fehlt er (oder ist ungültig), wirdsensor.{name}_hp{n}_compressor_thermal_energy_output_accumulatedverwendet.
Validierung in utils.validate_external_sensors: Beide Sensoren werden bei Angabe (State oder Entity Registry) geprüft. Ist nur der thermische ungültig, wird er verworfen und der thermische Default genutzt; der elektrische Eintrag bleibt erhalten.
Offsets¶
Offsets können für Total-Sensoren konfiguriert werden:
energy_consumption_offsets:
hp1:
heating_energy_total: 1000.0
heating_thermal_energy_total: 5000.0
# ...
Fehlerbehandlung¶
Sensor-Wechsel-Erkennung¶
- Elektrisch: Automatische Erkennung von Sensorwechseln (
sensor_ids,last_energy_readings), Nullwert-Schutz, Rückwärtssprung-Schutz; bei Wechsel_handle_sensor_change. - Thermisch: Gleiche Resilienz wie elektrisch:
thermal_sensor_ids,last_thermal_energy_readings,_handle_thermal_sensor_change; Persistierung incycle_energy_persist.json.
Zero-Value Protection¶
if current_energy_kwh == 0.0:
first_value_seen_dict[f"hp{hp_idx}"] = False
return # Warte auf ersten gültigen Wert
Overflow Protection¶
if current_energy_kwh < last_energy:
# Sensor wurde zurückgesetzt oder gewechselt
first_value_seen_dict[f"hp{hp_idx}"] = False
last_reading_dict[f"hp{hp_idx}"] = None
return
Negative Daily/Monthly/Yearly-Werte (Restore-Bug)¶
Symptom: current_daily_value (bzw. monthly/yearly) wird nach Neustart negativ angezeigt (z. B. -1305.88 kWh).
Ursache: Beim Persistieren speichert Home Assistant den State der Entity. Bei Daily-Sensoren ist der State der Anzeige-Wert (native_value = _energy_value - _yesterday_value), nicht der kumulative Total _energy_value. Beim Restore wurde früher _energy_value = float(last_state.state) gesetzt – also der Tageswert statt des Totals. Zusammen mit dem korrekt aus Attributen wiederhergestellten _yesterday_value ergibt sich dann: current_daily_value = _energy_value - _yesterday_value = (kleiner Tageswert) - (großer Vortag) = negativ.
Lösung in der Implementierung:
- Beim Restore von Daily/Monthly/Yearly-Sensoren wird
_energy_valuenicht mehr auslast_state.stateübernommen, sondern rekonstruiert: - Daily:
_energy_value = _yesterday_value + angezeigter State(bzw. aus Attributenergy_value, falls persistiert). - Monthly/Yearly: analog mit
_previous_monthly_value/_previous_yearly_value. - Der kumulative Total wird in den Attributen als
energy_valuemitpersistiert; beim nächsten Restore wird er direkt aus diesem Attribut gelesen, falls vorhanden.
Die Anzeige bleibt durch native_value = max(0.0, _energy_value - _yesterday_value) nach unten auf 0 begrenzt; durch die korrigierte Restore-Logik stimmen die internen Werte wieder und negative Werte treten nicht mehr auf.
Konsistenz Daily/Monthly/Yearly (yesterday/previous_* ≤ energy_value)¶
Problem: Nach Neustart können persistierte Daten (Recorder oder cycle_energy_persist) inkonsistent sein: yesterday_value bzw. previous_monthly_value / previous_yearly_value sind größer als energy_value. Dann wäre der Periodenwert (daily = energy_value − yesterday_value usw.) negativ.
Lösung in der Implementierung:
-
Restore (
restore_state): Nach dem Setzen von_energy_valueund_yesterday_value(bzw._previous_monthly_value/_previous_yearly_value) wird geprüft: Ist der Basis-Wert größer als_energy_value, wird er auf_energy_valuegesetzt (Korrektur + Log-Warnung). Die Rekonstruktion „displayed = yesterday + displayed“ wird nur ausgeführt, wenn_yesterday_value <= _energy_value(konsistent), damit kein Überschreiben mit falschem Wert erfolgt. -
Persist-Anwendung (
_apply_persisted_energy_state): Nach dem Übernehmen der Werte auscycle_energy_persistwird für Daily/Monthly/Yearly dieselbe Prüfung durchgeführt; bei Bedarf Korrektur und Warnung. -
Persist-Schreiben (Coordinator
_collect_energy_sensor_states): Beim Speichern incycle_energy_persistwird nie ein Paar mityesterday_valuebzw.previous_monthly_value/previous_yearly_valuegrößer alsenergy_valuegeschrieben; der Basis-Wert wird vor dem Schreiben aufenergy_valuebegrenzt. -
Daily-Init (
_initialize_daily_yesterday_value): Erkennt die Integration weiterhin negativen Tageswert (z. B. weil Total-Sensor beim Start noch nicht verfügbar war), setzt sieyesterday_value = energy_valueund markiert Persist als „dirty“, damit die Korrektur beim nächsten Zyklus mitgespeichert wird.
Damit können nach Neustart keine negativen Daily-/Monthly-/Yearly-Werte mehr aus inkonsistenten persistierten Daten entstehen; die Korrektur ist an Restore, Persist-Anwendung und Persist-Schreiben verankert.
Migration Electrical (erstes Release)¶
Beim ersten Start nach einem Update werden bestehende elektrische Daily-/Monthly-/Yearly-Sensoren beim Umstieg auf das Delta-Verfahren einmalig migriert: Fehlt in den persistierten Daten das Attribut energy_value, werden die Werte aus dem zugehörigen Total-Sensor abgeleitet (restore_state() nutzt last_state.state als Anzeigewert und setzt, falls der Total-Sensor verfügbar ist, _energy_value und _yesterday_value bzw. _previous_monthly_value / _previous_yearly_value entsprechend). Der angezeigte Tages-/Monats-/Jahreswert bleibt erhalten, keine negativen Werte. Danach greift die normale Restore-Logik (mit persistiertem energy_value). Thermische Sensoren benötigen diese Migration nicht (sie wurden mit dem Delta-Verfahren eingeführt).
Erweiterungen¶
Neue Betriebsmodi hinzufügen¶
- Modus zu
ENERGY_CONSUMPTION_MODESinconst.pyhinzufügen - Sensor-Templates in
ENERGY_CONSUMPTION_SENSOR_TEMPLATEShinzufügen - Modus-Mapping in
_track_hp_energy_type_consumptionerweitern
Neue Zeiträume hinzufügen¶
- Zeitraum zu
ENERGY_CONSUMPTION_PERIODSinconst.pyhinzufügen - Sensor-Templates für alle Modi hinzufügen
- Reset-Logik in
LambdaEnergyConsumptionSensorerweitern
Debugging¶
Logging¶
Aktiviere Debug-Logging für detaillierte Informationen:
logger:
default: info
logs:
custom_components.lambda_heat_pumps.coordinator: debug
custom_components.lambda_heat_pumps.utils: debug
Wichtige Log-Meldungen¶
DEBUG-010: Tracking startet für HPDEBUG-014: Energie-Offsets werden geladenEnergy counters updated: Sensoren wurden aktualisiertINTERNAL-SENSOR: Interner Modbus-Sensor wird verwendet
Tests¶
Tests befinden sich in tests/test_energy_consumption_sensors.py:
- Sensor-Erstellung
- Delta-Berechnung
- Period-Berechnung (daily, monthly, yearly)
- Offset-Anwendung
- Einheitenkonvertierung