From f57155396ec0002974076efa568c8ee3d2a73fdc Mon Sep 17 00:00:00 2001 From: seaspotter Date: Fri, 18 Sep 2026 14:23:01 +0200 Subject: [PATCH 1/2] Add Daikin, EGO and EMS-ESP consumer modules --- packages/modules/consumers/daikin/__init__.py | 0 .../daikin/daikin_homehub_air2air/__init__.py | 0 .../daikin/daikin_homehub_air2air/config.py | 34 ++++++++ .../daikin/daikin_homehub_air2air/consumer.py | 47 +++++++++++ packages/modules/consumers/daikin/vendor.py | 14 ++++ packages/modules/consumers/ego/__init__.py | 0 .../modules/consumers/ego/ego/__init__.py | 0 packages/modules/consumers/ego/ego/config.py | 32 ++++++++ .../modules/consumers/ego/ego/consumer.py | 63 +++++++++++++++ packages/modules/consumers/ego/vendor.py | 14 ++++ .../modules/consumers/ems_esp/__init__.py | 0 .../consumers/ems_esp/ems_esp/__init__.py | 0 .../consumers/ems_esp/ems_esp/config.py | 33 ++++++++ .../consumers/ems_esp/ems_esp/consumer.py | 79 +++++++++++++++++++ packages/modules/consumers/ems_esp/vendor.py | 14 ++++ 15 files changed, 330 insertions(+) create mode 100644 packages/modules/consumers/daikin/__init__.py create mode 100644 packages/modules/consumers/daikin/daikin_homehub_air2air/__init__.py create mode 100644 packages/modules/consumers/daikin/daikin_homehub_air2air/config.py create mode 100644 packages/modules/consumers/daikin/daikin_homehub_air2air/consumer.py create mode 100644 packages/modules/consumers/daikin/vendor.py create mode 100644 packages/modules/consumers/ego/__init__.py create mode 100644 packages/modules/consumers/ego/ego/__init__.py create mode 100644 packages/modules/consumers/ego/ego/config.py create mode 100644 packages/modules/consumers/ego/ego/consumer.py create mode 100644 packages/modules/consumers/ego/vendor.py create mode 100644 packages/modules/consumers/ems_esp/__init__.py create mode 100644 packages/modules/consumers/ems_esp/ems_esp/__init__.py create mode 100644 packages/modules/consumers/ems_esp/ems_esp/config.py create mode 100644 packages/modules/consumers/ems_esp/ems_esp/consumer.py create mode 100644 packages/modules/consumers/ems_esp/vendor.py diff --git a/packages/modules/consumers/daikin/__init__.py b/packages/modules/consumers/daikin/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/daikin/daikin_homehub_air2air/__init__.py b/packages/modules/consumers/daikin/daikin_homehub_air2air/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/daikin/daikin_homehub_air2air/config.py b/packages/modules/consumers/daikin/daikin_homehub_air2air/config.py new file mode 100644 index 0000000000..98c1f0dfb9 --- /dev/null +++ b/packages/modules/consumers/daikin/daikin_homehub_air2air/config.py @@ -0,0 +1,34 @@ +from typing import Optional, Tuple + +from control.consumer.consumer_data import ConsumerUsage +from helpermodules.auto_str import auto_str +from modules.common.consumer_setup import ConsumerSetup +from ..vendor import vendor_descriptor + + +@auto_str +class DaikinConfiguration: + def __init__(self, + ip_address: Optional[str] = None, + port: int = 502, + modbus_id: int = 1): + self.ip_address = ip_address + self.port = port + self.modbus_id = modbus_id + + +@auto_str +class Daikin(ConsumerSetup[DaikinConfiguration]): + def __init__(self, + name: str = "Daikin Altherma/Air2Air (HomeHub EKRHH)", + type: str = "daikin_homehub_air2air", + id: int = 0, + configuration: Optional[DaikinConfiguration] = None, + # Register gegen das offizielle EKRHH Installer Reference Guide (4PDE744838, + # Kap. 10 "Modbus TCP/IP oder RTU für Luft-zu-Luft-Wärmepumpe") geprüft. + usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_ONOFF, + ConsumerUsage.SUSPENDABLE_TUNABLE, + ConsumerUsage.METER_ONLY), + **kwargs) -> None: + super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( + ).type, configuration=configuration or DaikinConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/daikin/daikin_homehub_air2air/consumer.py b/packages/modules/consumers/daikin/daikin_homehub_air2air/consumer.py new file mode 100644 index 0000000000..601d4d1cc8 --- /dev/null +++ b/packages/modules/consumers/daikin/daikin_homehub_air2air/consumer.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +import logging +from typing import Optional + +from modules.common.abstract_device import DeviceDescriptor +from modules.common.configurable_consumer import ConfigurableConsumer, SetLimitData +from modules.common.modbus import ModbusDataType, ModbusTcpClient_ +from modules.consumers.daikin.daikin_homehub_air2air.config import Daikin + +log = logging.getLogger(__name__) + + +def create_consumer(config: Daikin): + client: Optional[ModbusTcpClient_] = None + + def initializer(): + nonlocal client + client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) + + def switch_on() -> None: + # Reg 1000: Smart-Grid-Modus, 2 = Empfehlung ein (Boost) + client.write_register(1000, 2, data_type=ModbusDataType.INT_16, unit=config.configuration.modbus_id) + + def switch_off() -> None: + # Reg 1000: Smart-Grid-Modus, 0 = freier Betrieb (Normal) + client.write_register(1000, 0, data_type=ModbusDataType.INT_16, unit=config.configuration.modbus_id) + + def set_power_limit(power_limit: Optional[float], data: SetLimitData) -> None: + # Reg 1001: Leistungsgrenze für Bedarfssteuerung, Pow16 (raw = kW * 100), lt. offiziellem + # Daikin-Referenzhandbuch (EKRHH Installer Reference Guide, Kap. 10.2.1/10.2). Bereich 0..20kW. + # Die Einheit setzt intern trotzdem eine Untergrenze von 40% der Nennleistung der + # Außeneinheit um - Werte darunter reduzieren die Leistung nicht weiter; echtes Abschalten + # läuft über switch_off() (Reg 1000, Zwangsabschaltung). + value = 0 if power_limit is None else round(max(power_limit, 0) / 10) + client.write_register(1001, value, data_type=ModbusDataType.INT_16, unit=config.configuration.modbus_id) + + # Keine update()-Funktion: die HomeHub-Anbindung stellt für Air2Air-Geräte keine + # Leistungsmessung zur Verfügung. Wer den Verbrauch erfassen will, braucht eine separate + # Leistungsmessung (zB Zwischenzähler/Smart-Plug), die diesem Verbraucher zugeordnet wird. + return ConfigurableConsumer(consumer_config=config, + initializer=initializer, + switch_on=switch_on, + switch_off=switch_off, + set_power_limit=set_power_limit) + + +device_descriptor = DeviceDescriptor(configuration_factory=Daikin) diff --git a/packages/modules/consumers/daikin/vendor.py b/packages/modules/consumers/daikin/vendor.py new file mode 100644 index 0000000000..1e0201b718 --- /dev/null +++ b/packages/modules/consumers/daikin/vendor.py @@ -0,0 +1,14 @@ +from pathlib import Path + +from modules.common.abstract_device import DeviceDescriptor +from modules.devices.vendors import VendorGroup + + +class Vendor: + def __init__(self): + self.type = Path(__file__).parent.name + self.vendor = "Daikin" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) diff --git a/packages/modules/consumers/ego/__init__.py b/packages/modules/consumers/ego/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/ego/ego/__init__.py b/packages/modules/consumers/ego/ego/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/ego/ego/config.py b/packages/modules/consumers/ego/ego/config.py new file mode 100644 index 0000000000..c0f2c6ea73 --- /dev/null +++ b/packages/modules/consumers/ego/ego/config.py @@ -0,0 +1,32 @@ +from typing import Optional, Tuple + +from control.consumer.consumer_data import ConsumerUsage +from helpermodules.auto_str import auto_str +from modules.common.consumer_setup import ConsumerSetup +from ..vendor import vendor_descriptor + + +@auto_str +class EgoConfiguration: + def __init__(self, + ip_address: Optional[str] = None, + port: int = 502, + modbus_id: int = 247): + self.ip_address = ip_address + self.port = port + self.modbus_id = modbus_id + + +@auto_str +class Ego(ConsumerSetup[EgoConfiguration]): + def __init__(self, + name: str = "E.G.O. Smart Heater", + type: str = "ego", + id: int = 0, + configuration: Optional[EgoConfiguration] = None, + usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_TUNABLE, + ConsumerUsage.SELF_CONTROLLED, + ConsumerUsage.METER_ONLY), + **kwargs) -> None: + super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( + ).type, configuration=configuration or EgoConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/ego/ego/consumer.py b/packages/modules/consumers/ego/ego/consumer.py new file mode 100644 index 0000000000..cf84e0a683 --- /dev/null +++ b/packages/modules/consumers/ego/ego/consumer.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +import logging +from typing import Optional + +from modules.common.abstract_consumer import CurrentValues +from modules.common.abstract_device import DeviceDescriptor +from modules.common.component_state import ConsumerState +from modules.common.component_type import ComponentType +from modules.common.configurable_consumer import ConfigurableConsumer, SetLimitData +from modules.common.modbus import ModbusDataType, ModbusTcpClient_ +from modules.common.simcount._simcounter import SimCounterConsumer +from modules.consumers.ego.ego.config import Ego + +log = logging.getLogger(__name__) + + +def create_consumer(config: Ego): + client: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None + + def initializer(): + nonlocal client, sim_counter + client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + + def send_values(values: CurrentValues) -> None: + unit = config.configuration.modbus_id + # Reg 4864 = -1: Heizstab nutzt selbst Reg 4865 (HomeTotalPower) zur Regelung. + client.write_register(4864, -1, data_type=ModbusDataType.INT_16, unit=unit) + # Reg 4865/4866: Netzsaldo, negativ = Überschuss (gleiche Konvention wie evu_power). + client.write_register(4865, round(values.evu_power), data_type=ModbusDataType.INT_32, unit=unit) + + def update() -> ConsumerState: + unit = config.configuration.modbus_id + status = client.read_holding_registers(5128, ModbusDataType.UINT_16, unit=unit) + power = 0 + if status & 0x01: + power += client.read_holding_registers(4096, ModbusDataType.UINT_16, unit=unit) + if status & 0x02: + power += client.read_holding_registers(4128, ModbusDataType.UINT_16, unit=unit) + if status & 0x04: + power += client.read_holding_registers(4160, ModbusDataType.UINT_16, unit=unit) + imported, exported = sim_counter.sim_count(power) + + return ConsumerState( + power=power, + imported=imported, + exported=exported + ) + + def set_power_limit(power_limit: Optional[float], data: SetLimitData) -> None: + # Reg 4864: gewünschte Leistung [W], bestmöglich auf die 500/1000/2000W-Relais verteilt. + value = 0 if power_limit is None else max(round(power_limit), 0) + client.write_register(4864, value, data_type=ModbusDataType.INT_16, unit=config.configuration.modbus_id) + + return ConfigurableConsumer(consumer_config=config, + initializer=initializer, + update=update, + send_values=send_values, + set_power_limit=set_power_limit) + + +device_descriptor = DeviceDescriptor(configuration_factory=Ego) diff --git a/packages/modules/consumers/ego/vendor.py b/packages/modules/consumers/ego/vendor.py new file mode 100644 index 0000000000..c6114229e0 --- /dev/null +++ b/packages/modules/consumers/ego/vendor.py @@ -0,0 +1,14 @@ +from pathlib import Path + +from modules.common.abstract_device import DeviceDescriptor +from modules.devices.vendors import VendorGroup + + +class Vendor: + def __init__(self): + self.type = Path(__file__).parent.name + self.vendor = "E.G.O." + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) diff --git a/packages/modules/consumers/ems_esp/__init__.py b/packages/modules/consumers/ems_esp/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/ems_esp/ems_esp/__init__.py b/packages/modules/consumers/ems_esp/ems_esp/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/ems_esp/ems_esp/config.py b/packages/modules/consumers/ems_esp/ems_esp/config.py new file mode 100644 index 0000000000..ba89be03e8 --- /dev/null +++ b/packages/modules/consumers/ems_esp/ems_esp/config.py @@ -0,0 +1,33 @@ +from typing import Optional, Tuple + +from control.consumer.consumer_data import ConsumerUsage +from helpermodules.auto_str import auto_str +from modules.common.consumer_setup import ConsumerSetup +from ..vendor import vendor_descriptor + + +@auto_str +class EmsEspConfiguration: + def __init__(self, + ip_address: Optional[str] = None, + token: Optional[str] = None): + self.ip_address = ip_address + self.token = token + + +@auto_str +class EmsEsp(ConsumerSetup[EmsEspConfiguration]): + def __init__(self, + name: str = "EMS-ESP Wärmepumpe (Bosch/Buderus/Junkers)", + type: str = "ems_esp", + id: int = 0, + configuration: Optional[EmsEspConfiguration] = None, + # kein SELF_CONTROLLED: die energyPriceEl/-PV-Entities steuern laut + # EMS-ESP32-Quellcode die Wärmequellen-Auswahl bei Hybridanlagen, nicht die + # Kompressorleistung einer reinen Wärmepumpe. + usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_ONOFF, + ConsumerUsage.SUSPENDABLE_TUNABLE, + ConsumerUsage.METER_ONLY), + **kwargs) -> None: + super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( + ).type, configuration=configuration or EmsEspConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/ems_esp/ems_esp/consumer.py b/packages/modules/consumers/ems_esp/ems_esp/consumer.py new file mode 100644 index 0000000000..802cbff11c --- /dev/null +++ b/packages/modules/consumers/ems_esp/ems_esp/consumer.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +import logging +from typing import Optional + +from modules.common import req +from modules.common.abstract_device import DeviceDescriptor +from modules.common.component_state import ConsumerState +from modules.common.component_type import ComponentType +from modules.common.configurable_consumer import ConfigurableConsumer, SetLimitData +from modules.common.simcount._simcounter import SimCounterConsumer +from modules.consumers.ems_esp.ems_esp.config import EmsEsp + +log = logging.getLogger(__name__) + + +def create_consumer(config: EmsEsp): + session: Optional[req.CustomSession] = None + sim_counter: Optional[SimCounterConsumer] = None + base_url: str = "" + headers: dict = {} + + def initializer(): + nonlocal session, sim_counter, base_url, headers + session = req.get_http_session() + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + # "heatpump" ist der EMS-ESP-Gerätename für eine reine Wärmepumpe; Hybridanlagen mit + # Gas-/Ölkessel laufen zB unter "boiler" (https://emsesp.org/All-Entities). + base_url = f"http://{config.configuration.ip_address}/api/heatpump" + headers = {"Content-Type": "application/json", + "Authorization": f"Bearer {config.configuration.token}"} + + def _write_entity(entity: str, value) -> None: + session.post(f"{base_url}/{entity}", json={"value": value}, headers=headers) + + def switch_on() -> None: + _write_entity("hpin4opt", "1xxxxxxxxxxx") + + def switch_off() -> None: + _write_entity("hpin4opt", "0xxxxxxxxxxx") + _write_entity("hpin1opt", "0xxxxxxxxxxxxxx") + + def update() -> ConsumerState: + # fehlender Schlüssel (zB falsches Modell) kommt als 200 OK zurück, nicht als HTTP-Fehler - + # ohne diese Prüfung würde das unbemerkt als 0W durchgehen. + values = session.get(base_url, headers=headers).json() + if "hpcurrpower" not in values: + raise ValueError( + f"Entity 'hpcurrpower' nicht in der Antwort von {base_url} enthalten - " + f"vorhandene Werte: {list(values.keys())}") + power = float(values["hpcurrpower"]) + imported, exported = sim_counter.sim_count(power) + + return ConsumerState( + power=power, + imported=imported, + exported=exported + ) + + def set_power_limit(power_limit: Optional[float], data: SetLimitData) -> None: + # pvmaxcomp wirkt nur bei aktivem SG-Ready-Boost (Eingang 4); bei SUSPENDABLE_TUNABLE ruft + # der Core nie switch_on, daher hier selbst mitschalten. Wirksamkeit von pvmaxcomp ist + # modellabhängig (EMS-ESP32 Diskussion #2062: funktioniert auf Buderus WLW186i-12E, nicht + # auf Bosch Compress CS7000iAW). + if power_limit is None: + _write_entity("hpin4opt", "0xxxxxxxxxxx") + _write_entity("hpin1opt", "0xxxxxxxxxxxxxx") + else: + _write_entity("hpin4opt", "1xxxxxxxxxxx") + _write_entity("pvmaxcomp", round(max(power_limit, 0) / 1000, 1)) + + return ConfigurableConsumer(consumer_config=config, + initializer=initializer, + update=update, + set_power_limit=set_power_limit, + switch_on=switch_on, + switch_off=switch_off) + + +device_descriptor = DeviceDescriptor(configuration_factory=EmsEsp) diff --git a/packages/modules/consumers/ems_esp/vendor.py b/packages/modules/consumers/ems_esp/vendor.py new file mode 100644 index 0000000000..aae2c507b2 --- /dev/null +++ b/packages/modules/consumers/ems_esp/vendor.py @@ -0,0 +1,14 @@ +from pathlib import Path + +from modules.common.abstract_device import DeviceDescriptor +from modules.devices.vendors import VendorGroup + + +class Vendor: + def __init__(self): + self.type = Path(__file__).parent.name + self.vendor = "EMS-ESP" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) From a97807a8c8fdabe739cadd8c2417fc7dd8e647c5 Mon Sep 17 00:00:00 2001 From: seaspotter Date: Thu, 24 Sep 2026 12:45:13 +0200 Subject: [PATCH 2/2] daikin: Air2Air-Fixes aus Review (Lena, PR #3969) + neues Altherma-Modul MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Air2Air: Name korrigiert ("Daikin Air2Air (HomeHub EKRHH)"), METER_ONLY entfernt (keine Leistungsmessung verfügbar). Neu: daikin_homehub_altherma - eigenes Modul für Daikin Altherma, da eigene Modbus-Register (EKRHH Installer Reference Guide 4PDE744838, Kap. 9, separat von Air2Air/Kap. 10). Register gegen das offizielle Handbuch geprüft: - switch_on/off: Reg 56 (Smart-Grid-Modus), analog zu Air2Airs Reg 1000 - set_power_limit: Reg 58 (Allgemeine Leistungsbegrenzung, gilt laut Handbuch unabhängig vom Smart-Grid-Modus, anders als Reg 57) - update: Input-Reg 51 (Stromverbrauch Wärmepumpe) - Altherma kann im Gegensatz zu Air2Air tatsächlich Leistung melden, daher METER_ONLY hier sinnvoll --- .../daikin/daikin_homehub_air2air/config.py | 5 +- .../daikin_homehub_altherma/__init__.py | 0 .../daikin/daikin_homehub_altherma/config.py | 34 +++++++++++ .../daikin_homehub_altherma/consumer.py | 59 +++++++++++++++++++ 4 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 packages/modules/consumers/daikin/daikin_homehub_altherma/__init__.py create mode 100644 packages/modules/consumers/daikin/daikin_homehub_altherma/config.py create mode 100644 packages/modules/consumers/daikin/daikin_homehub_altherma/consumer.py diff --git a/packages/modules/consumers/daikin/daikin_homehub_air2air/config.py b/packages/modules/consumers/daikin/daikin_homehub_air2air/config.py index 98c1f0dfb9..579b42552d 100644 --- a/packages/modules/consumers/daikin/daikin_homehub_air2air/config.py +++ b/packages/modules/consumers/daikin/daikin_homehub_air2air/config.py @@ -20,15 +20,14 @@ def __init__(self, @auto_str class Daikin(ConsumerSetup[DaikinConfiguration]): def __init__(self, - name: str = "Daikin Altherma/Air2Air (HomeHub EKRHH)", + name: str = "Daikin Air2Air (HomeHub EKRHH)", type: str = "daikin_homehub_air2air", id: int = 0, configuration: Optional[DaikinConfiguration] = None, # Register gegen das offizielle EKRHH Installer Reference Guide (4PDE744838, # Kap. 10 "Modbus TCP/IP oder RTU für Luft-zu-Luft-Wärmepumpe") geprüft. usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_ONOFF, - ConsumerUsage.SUSPENDABLE_TUNABLE, - ConsumerUsage.METER_ONLY), + ConsumerUsage.SUSPENDABLE_TUNABLE), **kwargs) -> None: super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( ).type, configuration=configuration or DaikinConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/daikin/daikin_homehub_altherma/__init__.py b/packages/modules/consumers/daikin/daikin_homehub_altherma/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/daikin/daikin_homehub_altherma/config.py b/packages/modules/consumers/daikin/daikin_homehub_altherma/config.py new file mode 100644 index 0000000000..1a60ecb5c7 --- /dev/null +++ b/packages/modules/consumers/daikin/daikin_homehub_altherma/config.py @@ -0,0 +1,34 @@ +from typing import Optional, Tuple + +from control.consumer.consumer_data import ConsumerUsage +from helpermodules.auto_str import auto_str +from modules.common.consumer_setup import ConsumerSetup +from ..vendor import vendor_descriptor + + +@auto_str +class DaikinAlthermaConfiguration: + def __init__(self, + ip_address: Optional[str] = None, + port: int = 502, + modbus_id: int = 1): + self.ip_address = ip_address + self.port = port + self.modbus_id = modbus_id + + +@auto_str +class DaikinAltherma(ConsumerSetup[DaikinAlthermaConfiguration]): + def __init__(self, + name: str = "Daikin Altherma (HomeHub)", + type: str = "daikin_homehub_altherma", + id: int = 0, + configuration: Optional[DaikinAlthermaConfiguration] = None, + # Register gegen das offizielle EKRHH Installer Reference Guide (4PDE744838, + # Kap. 9 "Modbus TCP/IP oder RTU für Daikin Altherma") geprüft. + usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_ONOFF, + ConsumerUsage.SUSPENDABLE_TUNABLE, + ConsumerUsage.METER_ONLY), + **kwargs) -> None: + super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( + ).type, configuration=configuration or DaikinAlthermaConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/daikin/daikin_homehub_altherma/consumer.py b/packages/modules/consumers/daikin/daikin_homehub_altherma/consumer.py new file mode 100644 index 0000000000..31ac5ebe9b --- /dev/null +++ b/packages/modules/consumers/daikin/daikin_homehub_altherma/consumer.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +import logging +from typing import Optional + +from modules.common.abstract_device import DeviceDescriptor +from modules.common.component_state import ConsumerState +from modules.common.component_type import ComponentType +from modules.common.configurable_consumer import ConfigurableConsumer, SetLimitData +from modules.common.modbus import ModbusDataType, ModbusTcpClient_ +from modules.common.simcount._simcounter import SimCounterConsumer +from modules.consumers.daikin.daikin_homehub_altherma.config import DaikinAltherma + +log = logging.getLogger(__name__) + + +def create_consumer(config: DaikinAltherma): + client: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None + + def initializer(): + nonlocal client, sim_counter + client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + + def update() -> ConsumerState: + # Reg 51: Stromverbrauch Wärmepumpe, Pow16 (raw = kW * 100) + power = client.read_input_registers( + 50, ModbusDataType.INT_16, unit=config.configuration.modbus_id) * 10 + imported, exported = sim_counter.sim_count(power) + return ConsumerState(power=power, imported=imported, exported=exported) + + def switch_on() -> None: + # Reg 56: Smart-Grid-Modus, 2 = Empfehlung ein (Boost) + client.write_register(55, 2, data_type=ModbusDataType.INT_16, unit=config.configuration.modbus_id) + + def switch_off() -> None: + # Reg 56: Smart-Grid-Modus, 0 = freier Betrieb (Normal) + client.write_register(55, 0, data_type=ModbusDataType.INT_16, unit=config.configuration.modbus_id) + + def set_power_limit(power_limit: Optional[float], data: SetLimitData) -> None: + # Reg 58: Allgemeine Leistungsbegrenzung, Pow16 (raw = kW * 100), lt. offiziellem + # Daikin-Referenzhandbuch (EKRHH Installer Reference Guide, Kap. 9.2.1/9.3). Bereich 0..20kW, + # gilt laut Referenzhandbuch unabhängig vom Smart-Grid-Modus (anders als Reg 57, das nur + # während der Pufferung im Modus "Empfehlung ein" greift). + # Die Einheit setzt intern trotzdem eine Untergrenze von 4,5kW für 15 Minuten nach + # Verdichterstart durch - Werte darunter reduzieren die Leistung in dieser Zeit nicht weiter; + # echtes Abschalten läuft über switch_off() (Reg 56, Zwangsabschaltung). + value = 0 if power_limit is None else round(max(power_limit, 0) / 10) + client.write_register(57, value, data_type=ModbusDataType.INT_16, unit=config.configuration.modbus_id) + + return ConfigurableConsumer(consumer_config=config, + initializer=initializer, + update=update, + switch_on=switch_on, + switch_off=switch_off, + set_power_limit=set_power_limit) + + +device_descriptor = DeviceDescriptor(configuration_factory=DaikinAltherma)