Energieverbrauchssensoren - Technische Dokumentation¶
Zuletzt geändert am 21.03.2026
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