From 4c1b8dc5bcbb021a6a476d5033b7373829fdc16b Mon Sep 17 00:00:00 2001 From: seaspotter Date: Thu, 30 Jul 2026 13:38:30 +0200 Subject: [PATCH 01/10] IDM: send PV/Hausverbrauch/Batterie via GLT statt Leistungsvorgabe --- packages/modules/consumers/idm/idm/config.py | 4 +- .../modules/consumers/idm/idm/consumer.py | 75 ++++++++++++------- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/packages/modules/consumers/idm/idm/config.py b/packages/modules/consumers/idm/idm/config.py index baf3c7bcc0..f110f65584 100644 --- a/packages/modules/consumers/idm/idm/config.py +++ b/packages/modules/consumers/idm/idm/config.py @@ -12,11 +12,13 @@ def __init__(self, ip_address: Optional[str] = None, port: Optional[int] = 502, modbus_id: Optional[int] = 1, - version: int = 1): + version: int = 1, + send_grid_values: bool = False): self.ip_address = ip_address self.port = port self.modbus_id = modbus_id self.version = version + self.send_grid_values = send_grid_values @auto_str diff --git a/packages/modules/consumers/idm/idm/consumer.py b/packages/modules/consumers/idm/idm/consumer.py index 792b412626..aefba45ae9 100644 --- a/packages/modules/consumers/idm/idm/consumer.py +++ b/packages/modules/consumers/idm/idm/consumer.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 from pymodbus.constants import Endian import logging + +from control import data from modules.common.abstract_device import DeviceDescriptor from modules.common.component_state import ConsumerState from modules.common.component_type import ComponentType @@ -24,42 +26,63 @@ def initializer(): def error_handler() -> None: initializer() + def send_values() -> None: + # Sendet die aktuellen Systemwerte an die IDM Navigator 2.0-Regelung über die + # Gebäudeleittechnik/Smartfox-Schnittstelle (Modbus TCP). IDM unterstützt keine + # direkte Leistungsvorgabe für die WP - die Regelung berechnet ihre Überschuss- und + # Speicherstrategie selbst anhand dieser Werte. Register-Layout siehe IDM + # "Gebäudeleittechnik-Smartfox.pdf", Kap. 2.2.5.1. Wird nur aufgerufen, wenn + # config.configuration.send_values aktiviert ist. + modbus_id = config.configuration.modbus_id + + # pv_all_data.get.power ist negativ, solange PV einspeist (gleiche Konvention wie beim + # EVU-Zähler) -> Vorzeichen drehen und auf 0 begrenzen, um die reine Erzeugung zu erhalten + pv_power = max(-data.data.pv_all_data.data.get.power, 0) + hausverbrauch = data.data.counter_all_data.data.set.home_consumption + ueberschuss = max(pv_power - hausverbrauch, 0) + + bat = data.data.bat_all_data + # Get.power ist bei Speichern bereits positiv = Entladung, negativ = Ladung -> passt + # direkt zur IDM-Registerdefinition "Batterieentladung" + battery_power = bat.data.get.power if bat.data.config.configured else 0 + battery_soc = bat.data.get.soc if bat.data.config.configured else None + + # Reg 74: Aktueller PV-Überschuss [kW] + client.write_register(74, ueberschuss / 1000, wordorder=Endian.Little, unit=modbus_id) + # Reg 78: Aktuelle PV Produktion [kW] + client.write_register(78, pv_power / 1000, wordorder=Endian.Little, unit=modbus_id) + # Reg 82: Hausverbrauch [kW], Default 0 + client.write_register(82, hausverbrauch / 1000, wordorder=Endian.Little, unit=modbus_id) + # Reg 84: Batterieentladung [kW], Default 0 (negativ = Ladung) + client.write_register(84, battery_power / 1000, wordorder=Endian.Little, unit=modbus_id) + # Reg 86: Batteriefüllstand [%], Default -1 (= kein Speicher) + client.write_register(86, int(battery_soc) if battery_soc is not None else -1, unit=modbus_id) + def update() -> ConsumerState: if config.configuration.version == 1: - power = client.read_holding_registers(4122, ModbusDataType.FLOAT_32, - unit=config.configuration.modbus_id) + power = client.read_holding_registers( + 4122, ModbusDataType.FLOAT_32, unit=config.configuration.modbus_id) else: - power = client.read_input_registers(4122, ModbusDataType.FLOAT_32, - unit=config.configuration.modbus_id) - power *= 100 + power = client.read_input_registers( + 4122, ModbusDataType.FLOAT_32, unit=config.configuration.modbus_id) + power *= 1000 # Register liefert kW -> openWB rechnet intern in W imported, exported = sim_counter.sim_count(power) + + if config.configuration.send_values: + send_values() + return ConsumerState( power=power, imported=imported, exported=exported ) - def set_limit(power_limit: float) -> None: - # braucht man Reg 78? der Wärmepumpe kann es doch egal sein, wie viel PV-Leistung vom Dach kommt, - # vlt soll die anderweitig genutzt werden - # - # - # muss man unterscheiden, ob Bezug als negative Zahl oder null übertragen werden soll. Die Wärmepumpe kann - # den Bezug ja nicht kompensieren. Bei 0W oder 500W Bezug soll das Verhalten gleich sein (Wärmepumpe nur - # nach Bedarf einschalten, Eigensteuerung) - # if config.configuration.send_import: - # power_limit = min(power_limit, MAX_VALUE_UINT32) - # power_limit = max(power_limit, 0) - # else: - # power_limit = min(power_limit, MAX_VALUE_INT16) - # power_limit = max(power_limit, MIN_VALUE_INT16) - - client.write_register(74, max(power_limit, 0), wordorder=Endian.Little, unit=config.configuration.modbus_id) - return ConfigurableConsumer(consumer_config=config, - initializer=initializer, - error_handler=error_handler, - update=update, - set_power_limit=set_limit,) + return ConfigurableConsumer( + consumer_config=config, + initializer=initializer, + error_handler=error_handler, + update=update, + ) device_descriptor = DeviceDescriptor(configuration_factory=Idm) From 2ae254a1e23d5254fd7461d43a084f6ac7d33c1c Mon Sep 17 00:00:00 2001 From: seaspotter Date: Thu, 30 Jul 2026 13:38:30 +0200 Subject: [PATCH 02/10] =?UTF-8?q?Lambda:=20PV-=C3=9Cberschuss=20statt=20po?= =?UTF-8?q?wer=5Flimit=20an=20E-Manager=20senden?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../consumers/lambda_/lambda_/config.py | 4 +-- .../consumers/lambda_/lambda_/consumer.py | 26 +++++++++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/modules/consumers/lambda_/lambda_/config.py b/packages/modules/consumers/lambda_/lambda_/config.py index 01b02a5970..5b25dc5d90 100644 --- a/packages/modules/consumers/lambda_/lambda_/config.py +++ b/packages/modules/consumers/lambda_/lambda_/config.py @@ -12,11 +12,11 @@ def __init__(self, ip_address: Optional[str] = None, port: Optional[int] = 502, modbus_id: Optional[int] = 1, - sign: int = 1): + send_values: bool = False): self.ip_address = ip_address self.port = port self.modbus_id = modbus_id - self.sign = sign + self.send_values = send_values @auto_str diff --git a/packages/modules/consumers/lambda_/lambda_/consumer.py b/packages/modules/consumers/lambda_/lambda_/consumer.py index 83063bbb0b..d63f3d3f6c 100644 --- a/packages/modules/consumers/lambda_/lambda_/consumer.py +++ b/packages/modules/consumers/lambda_/lambda_/consumer.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 from pymodbus.constants import Endian import logging + +from control import data from modules.common.abstract_device import DeviceDescriptor from modules.common.component_state import ConsumerState from modules.common.component_type import ComponentType @@ -24,26 +26,34 @@ def initializer(): def error_handler() -> None: initializer() + def send_values() -> None: + # Sendet den aktuellen PV-Überschuss an den Lambda E-Manager (Register 0102, + # Modbus-Protokoll 1.0, Kap. 3.2 und 4.3). Der EVU-Zähler in openWB ist positiv bei + # Bezug, negativ bei Einspeisung -> Vorzeichen drehen und auf 0 begrenzen, um den + # Überschuss als positiven Wert zu erhalten. Der E-Manager muss zusätzlich einmalig + # auf der Wärmepumpe selbst auf Datenquelle "Modbus Client" umgestellt werden (Kap. 4.3), + # das kann openWB nicht per Modbus setzen. + grid_power = data.data.counter_all_data.get_evu_counter().data.get.power + pv_surplus = max(-grid_power, 0) + client.write_register(102, pv_surplus, wordorder=Endian.Little, unit=config.configuration.modbus_id) + def update() -> ConsumerState: power = client.read_holding_registers(103, ModbusDataType.INT_16, unit=config.configuration.modbus_id) imported, exported = sim_counter.sim_count(power) + + if config.configuration.send_values: + send_values() + return ConsumerState( power=power, imported=imported, exported=exported ) - def set_limit(power_limit: float) -> None: - client.write_register(102, - max(power_limit * config.configuration.sign, 0), - wordorder=Endian.Little, - unit=config.configuration.modbus_id) - return ConfigurableConsumer(consumer_config=config, initializer=initializer, error_handler=error_handler, - update=update, - set_power_limit=set_limit,) + update=update,) device_descriptor = DeviceDescriptor(configuration_factory=Lambda) From cd7b7fef087d9973622f235e0028ee40c30881c1 Mon Sep 17 00:00:00 2001 From: seaspotter Date: Thu, 30 Jul 2026 13:38:30 +0200 Subject: [PATCH 03/10] Add OVUM CubeSpeicher/MPlus consumer module --- packages/modules/consumers/ovum/__init__.py | 0 .../modules/consumers/ovum/ovum/__init__.py | 0 .../modules/consumers/ovum/ovum/config.py | 33 +++++++ .../modules/consumers/ovum/ovum/consumer.py | 88 +++++++++++++++++++ packages/modules/consumers/ovum/vendor.py | 14 +++ 5 files changed, 135 insertions(+) create mode 100644 packages/modules/consumers/ovum/__init__.py create mode 100644 packages/modules/consumers/ovum/ovum/__init__.py create mode 100644 packages/modules/consumers/ovum/ovum/config.py create mode 100644 packages/modules/consumers/ovum/ovum/consumer.py create mode 100644 packages/modules/consumers/ovum/vendor.py diff --git a/packages/modules/consumers/ovum/__init__.py b/packages/modules/consumers/ovum/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/ovum/ovum/__init__.py b/packages/modules/consumers/ovum/ovum/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/ovum/ovum/config.py b/packages/modules/consumers/ovum/ovum/config.py new file mode 100644 index 0000000000..5ec4416011 --- /dev/null +++ b/packages/modules/consumers/ovum/ovum/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 OvumConfiguration: + def __init__(self, + ip_address: Optional[str] = None, + port: Optional[int] = 502, + modbus_id: Optional[int] = 1, + send_values: bool = False): + self.ip_address = ip_address + self.port = port + self.modbus_id = modbus_id + self.send_values = send_values + + +@auto_str +class Ovum(ConsumerSetup[OvumConfiguration]): + def __init__(self, + name: str = "OVUM Wärmepumpe (CubeSpeicher/MPlus)", + type: str = "ovum", + id: int = 0, + configuration: OvumConfiguration = None, + usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_TUNABLE, + ConsumerUsage.METER_ONLY), + **kwargs) -> None: + super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( + ).type, configuration=configuration or OvumConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/ovum/ovum/consumer.py b/packages/modules/consumers/ovum/ovum/consumer.py new file mode 100644 index 0000000000..a556137a44 --- /dev/null +++ b/packages/modules/consumers/ovum/ovum/consumer.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +from typing import Optional +import logging + +from control import data +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 +from modules.common.modbus import ModbusDataType, ModbusTcpClient_ +from modules.common.simcount._simcounter import SimCounterConsumer +from modules.consumers.ovum.ovum.config import Ovum + +log = logging.getLogger(__name__) + + +def create_consumer(config: Ovum): + client = None + sim_counter = None + last_mode = 'Undefined' + + def initializer(): + nonlocal client, sim_counter, last_mode + client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + last_mode = 'Undefined' + + def error_handler() -> None: + initializer() + + def send_values() -> None: + # Informativ: aktuellen Netzsaldo in Reg 710 schreiben, damit OVUM-eigene Anzeigen + # (PV-Watch Messwert Reg 456, Autarkiegrad Reg 450) auch im externen Leistungsvorgabe- + # Modus (Reg 709 = 2) einen sinnvollen Wert zeigen. UNVERIFIZIERT, ob Reg 710 dabei + # tatsächlich nur angezeigt und nicht doch in interne Berechnungen einfließt - vor + # Dauerbetrieb an echter Hardware prüfen, ob Reg 708 dadurch vom 711-Sollwert abweicht. + grid_power = data.data.counter_all_data.get_evu_counter().data.get.power + client.write_register(710, int(grid_power / 10), ModbusDataType.INT_16, + unit=config.configuration.modbus_id) + + def update() -> ConsumerState: + # Reg 708: elektrische Leistungsaufnahme Wärmepumpe [kW]*100 -> Watt = Rohwert * 10 + power = client.read_holding_registers(708, ModbusDataType.INT_16, + unit=config.configuration.modbus_id) * 10 + imported, exported = sim_counter.sim_count(power) + + if config.configuration.send_values: + send_values() + + return ConsumerState( + power=power, + imported=imported, + exported=exported + ) + + def set_limit(power_limit: Optional[float]) -> None: + nonlocal last_mode + unit = config.configuration.modbus_id + + if power_limit is None: + # Keine Leistungsvorgabe gefordert, Ovum-eigene PV-Watch-Regelung übernimmt wieder + if last_mode != 'self': + client.write_register(709, 0, ModbusDataType.INT_16, unit=unit) + log.debug("Keine Leistungsvorgabe, Ovum-eigene PV-Watch-Regelung aktivieren.") + last_mode = 'self' + else: + # Leistungsvorgabe gefordert: externe Steuerung aktivieren (falls noch nicht aktiv) + # und den absoluten Ziel-Leistungsbezug in Reg 711 schreiben. power_limit ist bereits + # von openWB berechnet und berücksichtigt die aktuelle WP-Leistungsaufnahme - bewusst + # NICHT Reg 710 (PV-Watch TCP, roher Netzsaldo) als Regelgröße verwendet, da das laut + # evcc-Issue #28372 einen Regelkreis-Konflikt erzeugt (WP-eigene Leistung ist im + # Saldo bereits enthalten, WP-Regler und openWB-Regler können gegeneinander + # aufschaukeln). + if last_mode != 'limited': + client.write_register(709, 2, ModbusDataType.INT_16, unit=unit) + log.debug("Leistungsvorgabe gefordert, externe Steuerung (Reg 711) aktivieren.") + last_mode = 'limited' + raw_value = int(max(power_limit, 0) / 10) # W -> [kW]*100 + client.write_register(711, raw_value, ModbusDataType.INT_16, unit=unit) + + return ConfigurableConsumer(consumer_config=config, + initializer=initializer, + error_handler=error_handler, + update=update, + set_power_limit=set_limit,) + + +device_descriptor = DeviceDescriptor(configuration_factory=Ovum) diff --git a/packages/modules/consumers/ovum/vendor.py b/packages/modules/consumers/ovum/vendor.py new file mode 100644 index 0000000000..0523a05cf4 --- /dev/null +++ b/packages/modules/consumers/ovum/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 = "Ovum" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) From d271866c4e149300d6a54c96b3e04fa85d6f96e2 Mon Sep 17 00:00:00 2001 From: seaspotter Date: Thu, 30 Jul 2026 13:48:34 +0200 Subject: [PATCH 04/10] IDM: revert scaling factor, fix flake8 formatting --- packages/modules/consumers/idm/idm/consumer.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/modules/consumers/idm/idm/consumer.py b/packages/modules/consumers/idm/idm/consumer.py index aefba45ae9..011aa4ab47 100644 --- a/packages/modules/consumers/idm/idm/consumer.py +++ b/packages/modules/consumers/idm/idm/consumer.py @@ -65,7 +65,7 @@ def update() -> ConsumerState: else: power = client.read_input_registers( 4122, ModbusDataType.FLOAT_32, unit=config.configuration.modbus_id) - power *= 1000 # Register liefert kW -> openWB rechnet intern in W + power *= 100 imported, exported = sim_counter.sim_count(power) if config.configuration.send_values: @@ -77,12 +77,10 @@ def update() -> ConsumerState: exported=exported ) - return ConfigurableConsumer( - consumer_config=config, - initializer=initializer, - error_handler=error_handler, - update=update, - ) + return ConfigurableConsumer(consumer_config=config, + initializer=initializer, + error_handler=error_handler, + update=update) device_descriptor = DeviceDescriptor(configuration_factory=Idm) From 8f878f4f9944de179c627965ac4e90dd7379a8a6 Mon Sep 17 00:00:00 2001 From: seaspotter Date: Thu, 30 Jul 2026 13:50:42 +0200 Subject: [PATCH 05/10] flake8 --- packages/modules/consumers/idm/idm/consumer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/modules/consumers/idm/idm/consumer.py b/packages/modules/consumers/idm/idm/consumer.py index 011aa4ab47..51e3464cbc 100644 --- a/packages/modules/consumers/idm/idm/consumer.py +++ b/packages/modules/consumers/idm/idm/consumer.py @@ -60,11 +60,11 @@ def send_values() -> None: def update() -> ConsumerState: if config.configuration.version == 1: - power = client.read_holding_registers( - 4122, ModbusDataType.FLOAT_32, unit=config.configuration.modbus_id) + power = client.read_holding_registers(4122, ModbusDataType.FLOAT_32, + unit=config.configuration.modbus_id) else: - power = client.read_input_registers( - 4122, ModbusDataType.FLOAT_32, unit=config.configuration.modbus_id) + power = client.read_input_registers(4122, ModbusDataType.FLOAT_32, + unit=config.configuration.modbus_id) power *= 100 imported, exported = sim_counter.sim_count(power) From 3ad8f446be47f5d81a6d269e7c95badbf7eba74d Mon Sep 17 00:00:00 2001 From: SeaSpotter Date: Thu, 30 Jul 2026 14:17:37 +0200 Subject: [PATCH 06/10] Rename send_grid_values to send_values in config.py --- packages/modules/consumers/idm/idm/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/modules/consumers/idm/idm/config.py b/packages/modules/consumers/idm/idm/config.py index f110f65584..00608f5240 100644 --- a/packages/modules/consumers/idm/idm/config.py +++ b/packages/modules/consumers/idm/idm/config.py @@ -13,12 +13,12 @@ def __init__(self, port: Optional[int] = 502, modbus_id: Optional[int] = 1, version: int = 1, - send_grid_values: bool = False): + send_values: bool = False): self.ip_address = ip_address self.port = port self.modbus_id = modbus_id self.version = version - self.send_grid_values = send_grid_values + self.send_values = send_values @auto_str From 04f6fa424fe8e231d0e7448889143e7f49763150 Mon Sep 17 00:00:00 2001 From: SeaSpotter Date: Thu, 30 Jul 2026 14:27:14 +0200 Subject: [PATCH 07/10] Adjust surplus calculation to account for charging power Include charging power in surplus calculation to avoid misreporting surplus to the heat pump. --- packages/modules/consumers/idm/idm/consumer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/modules/consumers/idm/idm/consumer.py b/packages/modules/consumers/idm/idm/consumer.py index 51e3464cbc..71a14ce29f 100644 --- a/packages/modules/consumers/idm/idm/consumer.py +++ b/packages/modules/consumers/idm/idm/consumer.py @@ -39,7 +39,10 @@ def send_values() -> None: # EVU-Zähler) -> Vorzeichen drehen und auf 0 begrenzen, um die reine Erzeugung zu erhalten pv_power = max(-data.data.pv_all_data.data.get.power, 0) hausverbrauch = data.data.counter_all_data.data.set.home_consumption - ueberschuss = max(pv_power - hausverbrauch, 0) + # Ladepunkt-Leistung mit einrechnen, da diese sonst fälschlich als freier Überschuss an die + # WP gemeldet würde, obwohl sie bereits von einer Fahrzeugladung verbraucht wird + ladeleistung = data.data.cp_all_data.data.get.power + ueberschuss = max(pv_power - hausverbrauch - ladeleistung, 0) bat = data.data.bat_all_data # Get.power ist bei Speichern bereits positiv = Entladung, negativ = Ladung -> passt From 9702e679956c26b0951d899bb89bc2c70b319307 Mon Sep 17 00:00:00 2001 From: SeaSpotter Date: Thu, 30 Jul 2026 14:56:53 +0200 Subject: [PATCH 08/10] Update send_values function to send grid power --- packages/modules/consumers/lambda_/lambda_/consumer.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/modules/consumers/lambda_/lambda_/consumer.py b/packages/modules/consumers/lambda_/lambda_/consumer.py index d63f3d3f6c..b90024bf0c 100644 --- a/packages/modules/consumers/lambda_/lambda_/consumer.py +++ b/packages/modules/consumers/lambda_/lambda_/consumer.py @@ -27,15 +27,12 @@ def error_handler() -> None: initializer() def send_values() -> None: - # Sendet den aktuellen PV-Überschuss an den Lambda E-Manager (Register 0102, - # Modbus-Protokoll 1.0, Kap. 3.2 und 4.3). Der EVU-Zähler in openWB ist positiv bei - # Bezug, negativ bei Einspeisung -> Vorzeichen drehen und auf 0 begrenzen, um den - # Überschuss als positiven Wert zu erhalten. Der E-Manager muss zusätzlich einmalig + # Sendet die aktuelle EVU Leistung an den Lambda E-Manager (Register 0102, + # Modbus-Protokoll 1.0, Kap. 3.2 und 4.3). Der E-Manager muss zusätzlich einmalig # auf der Wärmepumpe selbst auf Datenquelle "Modbus Client" umgestellt werden (Kap. 4.3), # das kann openWB nicht per Modbus setzen. grid_power = data.data.counter_all_data.get_evu_counter().data.get.power - pv_surplus = max(-grid_power, 0) - client.write_register(102, pv_surplus, wordorder=Endian.Little, unit=config.configuration.modbus_id) + client.write_register(102, grid_power, wordorder=Endian.Little, unit=config.configuration.modbus_id) def update() -> ConsumerState: power = client.read_holding_registers(103, ModbusDataType.INT_16, unit=config.configuration.modbus_id) From b4e5b6d74e5db2cd56279a402739a71a2c6967db Mon Sep 17 00:00:00 2001 From: seaspotter Date: Tue, 11 Aug 2026 21:52:04 +0200 Subject: [PATCH 09/10] Add Self_Controlled for IDM and Lambda --- packages/modules/consumers/idm/idm/config.py | 5 +- .../modules/consumers/idm/idm/consumer.py | 86 ++++++++++--------- .../consumers/lambda_/lambda_/config.py | 5 +- .../consumers/lambda_/lambda_/consumer.py | 54 +++++++----- .../modules/consumers/ovum/ovum/config.py | 20 +++-- .../modules/consumers/ovum/ovum/consumer.py | 85 +++++++++++++----- 6 files changed, 154 insertions(+), 101 deletions(-) diff --git a/packages/modules/consumers/idm/idm/config.py b/packages/modules/consumers/idm/idm/config.py index 00608f5240..e417129637 100644 --- a/packages/modules/consumers/idm/idm/config.py +++ b/packages/modules/consumers/idm/idm/config.py @@ -12,13 +12,11 @@ def __init__(self, ip_address: Optional[str] = None, port: Optional[int] = 502, modbus_id: Optional[int] = 1, - version: int = 1, - send_values: bool = False): + version: int = 1): self.ip_address = ip_address self.port = port self.modbus_id = modbus_id self.version = version - self.send_values = send_values @auto_str @@ -29,6 +27,7 @@ def __init__(self, id: int = 0, configuration: IdmConfiguration = 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( diff --git a/packages/modules/consumers/idm/idm/consumer.py b/packages/modules/consumers/idm/idm/consumer.py index 71a14ce29f..c2a67d0e87 100644 --- a/packages/modules/consumers/idm/idm/consumer.py +++ b/packages/modules/consumers/idm/idm/consumer.py @@ -1,8 +1,9 @@ + #!/usr/bin/env python3 from pymodbus.constants import Endian import logging - -from control import data + +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 @@ -10,80 +11,81 @@ from modules.common.modbus import ModbusDataType, ModbusTcpClient_ from modules.common.simcount._simcounter import SimCounterConsumer from modules.consumers.idm.idm.config import Idm - + log = logging.getLogger(__name__) - - + + def create_consumer(config: Idm): client = None sim_counter = None - + def initializer(): nonlocal client, sim_counter client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) - + def error_handler() -> None: initializer() - - def send_values() -> None: + + def send_values(values: CurrentValues) -> None: # Sendet die aktuellen Systemwerte an die IDM Navigator 2.0-Regelung über die # Gebäudeleittechnik/Smartfox-Schnittstelle (Modbus TCP). IDM unterstützt keine # direkte Leistungsvorgabe für die WP - die Regelung berechnet ihre Überschuss- und - # Speicherstrategie selbst anhand dieser Werte. Register-Layout siehe IDM - # "Gebäudeleittechnik-Smartfox.pdf", Kap. 2.2.5.1. Wird nur aufgerufen, wenn - # config.configuration.send_values aktiviert ist. + # Speicherstrategie selbst anhand dieser Werte. Wird vom Core automatisch aufgerufen, + # solange dieser Verbraucher als ConsumerUsage.SELF_CONTROLLED konfiguriert ist. + # Register-Layout siehe IDM "Gebäudeleittechnik-Smartfox.pdf", Kap. 2.2.5.1 modbus_id = config.configuration.modbus_id - - # pv_all_data.get.power ist negativ, solange PV einspeist (gleiche Konvention wie beim - # EVU-Zähler) -> Vorzeichen drehen und auf 0 begrenzen, um die reine Erzeugung zu erhalten - pv_power = max(-data.data.pv_all_data.data.get.power, 0) - hausverbrauch = data.data.counter_all_data.data.set.home_consumption - # Ladepunkt-Leistung mit einrechnen, da diese sonst fälschlich als freier Überschuss an die + + # values.pv_power kommt roh aus process.py (negativ während Produktion, gleiche + # Konvention wie beim EVU-Zähler) -> Vorzeichen drehen und auf 0 begrenzen, um die + # reine Erzeugung zu erhalten + pv_power = max(-values.pv_power, 0) + # Ladeleistung mit einrechnen, da diese sonst fälschlich als freier Überschuss an die # WP gemeldet würde, obwohl sie bereits von einer Fahrzeugladung verbraucht wird - ladeleistung = data.data.cp_all_data.data.get.power - ueberschuss = max(pv_power - hausverbrauch - ladeleistung, 0) - - bat = data.data.bat_all_data - # Get.power ist bei Speichern bereits positiv = Entladung, negativ = Ladung -> passt - # direkt zur IDM-Registerdefinition "Batterieentladung" - battery_power = bat.data.get.power if bat.data.config.configured else 0 - battery_soc = bat.data.get.soc if bat.data.config.configured else None - + surplus = max(pv_power - values.home_consumption - values.cp_power, 0) + battery_soc = int(values.bat_soc) if values.bat_soc is not None else -1 + # Reg 74: Aktueller PV-Überschuss [kW] - client.write_register(74, ueberschuss / 1000, wordorder=Endian.Little, unit=modbus_id) - # Reg 78: Aktuelle PV Produktion [kW] + client.write_register(74, surplus / 1000, wordorder=Endian.Little, unit=modbus_id) + # Reg 78: Aktuelle PV-Produktion [kW] client.write_register(78, pv_power / 1000, wordorder=Endian.Little, unit=modbus_id) # Reg 82: Hausverbrauch [kW], Default 0 - client.write_register(82, hausverbrauch / 1000, wordorder=Endian.Little, unit=modbus_id) + client.write_register(82, values.home_consumption / 1000, wordorder=Endian.Little, unit=modbus_id) # Reg 84: Batterieentladung [kW], Default 0 (negativ = Ladung) - client.write_register(84, battery_power / 1000, wordorder=Endian.Little, unit=modbus_id) + client.write_register(84, values.bat_power / 1000, wordorder=Endian.Little, unit=modbus_id) # Reg 86: Batteriefüllstand [%], Default -1 (= kein Speicher) - client.write_register(86, int(battery_soc) if battery_soc is not None else -1, unit=modbus_id) - + client.write_register(86, battery_soc, unit=modbus_id) + def update() -> ConsumerState: if config.configuration.version == 1: - power = client.read_holding_registers(4122, ModbusDataType.FLOAT_32, - unit=config.configuration.modbus_id) + power = client.read_holding_registers( + 4122, ModbusDataType.FLOAT_32, unit=config.configuration.modbus_id) else: - power = client.read_input_registers(4122, ModbusDataType.FLOAT_32, - unit=config.configuration.modbus_id) + power = client.read_input_registers( + 4122, ModbusDataType.FLOAT_32, unit=config.configuration.modbus_id) power *= 100 imported, exported = sim_counter.sim_count(power) - - if config.configuration.send_values: - send_values() - + return ConsumerState( power=power, imported=imported, exported=exported ) + def set_limit(power_limit: float) -> None: + # Schreibt den von openWB berechneten Überschuss in Register 74 (PV-Überschuss [kW]). + # Keine echte Leistungsvorgabe - die IDM-Regelung berechnet ihre Strategie selbst anhand dieses Wertes. + # Hinweis: Dies überschreibt den intern berechneten PV-Überschuss der IDM-Regelung und kann + # die PV-Eigenverbrauchs-Statistik im IDM-Portal verfälschen. + client.write_register(74, max(power_limit, 0) / 1000, wordorder=Endian.Little, + unit=config.configuration.modbus_id) + return ConfigurableConsumer(consumer_config=config, initializer=initializer, error_handler=error_handler, - update=update) + update=update, + send_values=send_values, + set_power_limit=set_limit) device_descriptor = DeviceDescriptor(configuration_factory=Idm) diff --git a/packages/modules/consumers/lambda_/lambda_/config.py b/packages/modules/consumers/lambda_/lambda_/config.py index 5b25dc5d90..c02249395f 100644 --- a/packages/modules/consumers/lambda_/lambda_/config.py +++ b/packages/modules/consumers/lambda_/lambda_/config.py @@ -11,12 +11,10 @@ class LambdaConfiguration: def __init__(self, ip_address: Optional[str] = None, port: Optional[int] = 502, - modbus_id: Optional[int] = 1, - send_values: bool = False): + modbus_id: Optional[int] = 1): self.ip_address = ip_address self.port = port self.modbus_id = modbus_id - self.send_values = send_values @auto_str @@ -27,6 +25,7 @@ def __init__(self, id: int = 0, configuration: LambdaConfiguration = 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( diff --git a/packages/modules/consumers/lambda_/lambda_/consumer.py b/packages/modules/consumers/lambda_/lambda_/consumer.py index b90024bf0c..a673238a7f 100644 --- a/packages/modules/consumers/lambda_/lambda_/consumer.py +++ b/packages/modules/consumers/lambda_/lambda_/consumer.py @@ -1,8 +1,10 @@ + +Lambda consumer · PY #!/usr/bin/env python3 from pymodbus.constants import Endian import logging - -from control import data + +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 @@ -10,47 +12,55 @@ from modules.common.modbus import ModbusDataType, ModbusTcpClient_ from modules.common.simcount._simcounter import SimCounterConsumer from modules.consumers.lambda_.lambda_.config import Lambda - + log = logging.getLogger(__name__) - - + + def create_consumer(config: Lambda): client = None sim_counter = None - + def initializer(): nonlocal client, sim_counter client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) - + def error_handler() -> None: initializer() - - def send_values() -> None: - # Sendet die aktuelle EVU Leistung an den Lambda E-Manager (Register 0102, - # Modbus-Protokoll 1.0, Kap. 3.2 und 4.3). Der E-Manager muss zusätzlich einmalig - # auf der Wärmepumpe selbst auf Datenquelle "Modbus Client" umgestellt werden (Kap. 4.3), - # das kann openWB nicht per Modbus setzen. - grid_power = data.data.counter_all_data.get_evu_counter().data.get.power - client.write_register(102, grid_power, wordorder=Endian.Little, unit=config.configuration.modbus_id) - + + def send_values(values: CurrentValues) -> None: + # Sendet die aktuelle EVU-Leistung an den Lambda E-Manager (Modbus-Protokoll 1.0, + # Kap. 3.2 und 4.3, Reg 102). Der E-Manager muss zusätzlich einmalig auf der + # Wärmepumpe selbst auf Datenquelle "Modbus Client" umgestellt werden (Kap. 4.3), + # das kann openWB nicht per Modbus setzen. Wird vom Core automatisch aufgerufen, + # solange dieser Verbraucher als ConsumerUsage.SELF_CONTROLLED konfiguriert ist. + client.write_register(102, values.evu_power, wordorder=Endian.Little, unit=config.configuration.modbus_id) + def update() -> ConsumerState: - power = client.read_holding_registers(103, ModbusDataType.INT_16, unit=config.configuration.modbus_id) + power = client.read_holding_registers( + 103, ModbusDataType.INT_16, unit=config.configuration.modbus_id) imported, exported = sim_counter.sim_count(power) - - if config.configuration.send_values: - send_values() - + return ConsumerState( power=power, imported=imported, exported=exported ) + def set_limit(power_limit: float) -> None: + # Schreibt den von openWB berechneten Überschuss in Register 102 (E-Manager Bezug/Einspeisung). + # Keine echte Leistungsvorgabe - die Wärmepumpe regelt weiterhin selbst anhand dieses Wertes. + client.write_register(102, + max(power_limit, 0), + wordorder=Endian.Little, + unit=config.configuration.modbus_id) + return ConfigurableConsumer(consumer_config=config, initializer=initializer, error_handler=error_handler, - update=update,) + update=update, + send_values=send_values, + set_power_limit=set_limit,) device_descriptor = DeviceDescriptor(configuration_factory=Lambda) diff --git a/packages/modules/consumers/ovum/ovum/config.py b/packages/modules/consumers/ovum/ovum/config.py index 5ec4416011..6c6e543f3f 100644 --- a/packages/modules/consumers/ovum/ovum/config.py +++ b/packages/modules/consumers/ovum/ovum/config.py @@ -1,24 +1,22 @@ 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 OvumConfiguration: def __init__(self, ip_address: Optional[str] = None, port: Optional[int] = 502, - modbus_id: Optional[int] = 1, - send_values: bool = False): + modbus_id: Optional[int] = 1): self.ip_address = ip_address self.port = port self.modbus_id = modbus_id - self.send_values = send_values - - + + @auto_str class Ovum(ConsumerSetup[OvumConfiguration]): def __init__(self, @@ -26,8 +24,14 @@ def __init__(self, type: str = "ovum", id: int = 0, configuration: OvumConfiguration = None, + # OVUM unterstützt eine echte Leistungsvorgabe (SUSPENDABLE_TUNABLE), + # Eigenregelung anhand der Systemwerte (SELF_CONTROLLED) sowie + # SG-Ready-Ein-/Ausschalten (SUSPENDABLE_ONOFF) usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_TUNABLE, + ConsumerUsage.SELF_CONTROLLED, + ConsumerUsage.SUSPENDABLE_ONOFF, ConsumerUsage.METER_ONLY), **kwargs) -> None: super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( ).type, configuration=configuration or OvumConfiguration(), usage=usage, **kwargs) + \ No newline at end of file diff --git a/packages/modules/consumers/ovum/ovum/consumer.py b/packages/modules/consumers/ovum/ovum/consumer.py index a556137a44..67fbe9570d 100644 --- a/packages/modules/consumers/ovum/ovum/consumer.py +++ b/packages/modules/consumers/ovum/ovum/consumer.py @@ -2,7 +2,7 @@ from typing import Optional import logging -from control import data +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 @@ -17,26 +17,30 @@ def create_consumer(config: Ovum): client = None sim_counter = None - last_mode = 'Undefined' def initializer(): - nonlocal client, sim_counter, last_mode + nonlocal client, sim_counter client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) - last_mode = 'Undefined' def error_handler() -> None: initializer() - def send_values() -> None: - # Informativ: aktuellen Netzsaldo in Reg 710 schreiben, damit OVUM-eigene Anzeigen - # (PV-Watch Messwert Reg 456, Autarkiegrad Reg 450) auch im externen Leistungsvorgabe- - # Modus (Reg 709 = 2) einen sinnvollen Wert zeigen. UNVERIFIZIERT, ob Reg 710 dabei - # tatsächlich nur angezeigt und nicht doch in interne Berechnungen einfließt - vor - # Dauerbetrieb an echter Hardware prüfen, ob Reg 708 dadurch vom 711-Sollwert abweicht. - grid_power = data.data.counter_all_data.get_evu_counter().data.get.power - client.write_register(710, int(grid_power / 10), ModbusDataType.INT_16, - unit=config.configuration.modbus_id) + def send_values(values: CurrentValues) -> None: + # ConsumerUsage.SELF_CONTROLLED: aktiviert Ovum-eigene PV-Watch-Regelung (Reg 709 = 0) + # und schreibt informativ den aktuellen Netzsaldo in Reg 710, damit OVUM-eigene + # Anzeigen (PV-Watch Messwert Reg 456, Autarkiegrad Reg 450) einen sinnvollen Wert + # zeigen. UNVERIFIZIERT, ob Reg 710 dabei tatsächlich nur angezeigt und nicht doch in + # interne Berechnungen einfließt - vor Dauerbetrieb an echter Hardware prüfen, ob + # Reg 708 dadurch vom erwarteten Eigenregelungs-Verhalten abweicht. Vor jedem Schreiben + # wird der Ist-Zustand gelesen, damit unnötige Moduswechsel-Befehle vermieden werden. + unit = config.configuration.modbus_id + + mode = client.read_holding_registers(709, ModbusDataType.INT_16, unit=unit) + if mode != 0: + client.write_register(709, 0, ModbusDataType.INT_16, unit=unit) + log.debug("Wärmepumpe in Eigenregelung, Ovum-eigene PV-Watch-Regelung aktivieren.") + client.write_register(710, int(values.evu_power / 10), ModbusDataType.INT_16, unit=unit) def update() -> ConsumerState: # Reg 708: elektrische Leistungsaufnahme Wärmepumpe [kW]*100 -> Watt = Rohwert * 10 @@ -44,9 +48,6 @@ def update() -> ConsumerState: unit=config.configuration.modbus_id) * 10 imported, exported = sim_counter.sim_count(power) - if config.configuration.send_values: - send_values() - return ConsumerState( power=power, imported=imported, @@ -54,15 +55,16 @@ def update() -> ConsumerState: ) def set_limit(power_limit: Optional[float]) -> None: - nonlocal last_mode + # ConsumerUsage.SUSPENDABLE_TUNABLE: externe Leistungsvorgabe. Ist-Modus wird vor dem + # Schreiben gelesen, um unnötige Moduswechsel-Befehle zu vermeiden. unit = config.configuration.modbus_id + mode = client.read_holding_registers(709, ModbusDataType.INT_16, unit=unit) if power_limit is None: # Keine Leistungsvorgabe gefordert, Ovum-eigene PV-Watch-Regelung übernimmt wieder - if last_mode != 'self': + if mode != 0: client.write_register(709, 0, ModbusDataType.INT_16, unit=unit) log.debug("Keine Leistungsvorgabe, Ovum-eigene PV-Watch-Regelung aktivieren.") - last_mode = 'self' else: # Leistungsvorgabe gefordert: externe Steuerung aktivieren (falls noch nicht aktiv) # und den absoluten Ziel-Leistungsbezug in Reg 711 schreiben. power_limit ist bereits @@ -71,18 +73,55 @@ def set_limit(power_limit: Optional[float]) -> None: # evcc-Issue #28372 einen Regelkreis-Konflikt erzeugt (WP-eigene Leistung ist im # Saldo bereits enthalten, WP-Regler und openWB-Regler können gegeneinander # aufschaukeln). - if last_mode != 'limited': + if mode != 2: client.write_register(709, 2, ModbusDataType.INT_16, unit=unit) log.debug("Leistungsvorgabe gefordert, externe Steuerung (Reg 711) aktivieren.") - last_mode = 'limited' raw_value = int(max(power_limit, 0) / 10) # W -> [kW]*100 client.write_register(711, raw_value, ModbusDataType.INT_16, unit=unit) + def switch_on() -> None: + # ConsumerUsage.SUSPENDABLE_ONOFF: SG-Ready "Empfehlung" (Kontakt 1 = 0, Kontakt 2 = 1) - + # die WP darf eigene Prioritäten (Legionellenschutz, Mindestlaufzeiten) weiter + # berücksichtigen, wird aber zum Einschalten angeregt. Ist-Zustand wird vor jedem + # Schreiben gelesen, um unnötige Befehle zu vermeiden. + unit = config.configuration.modbus_id + + sg_ready_mode = client.read_holding_registers(1250, ModbusDataType.INT_16, unit=unit) + if sg_ready_mode != 1: + client.write_register(1250, 1, ModbusDataType.INT_16, unit=unit) # SG-Ready per TCP + + contact_1 = client.read_holding_registers(1251, ModbusDataType.INT_16, unit=unit) + if contact_1 != 0: + client.write_register(1251, 0, ModbusDataType.INT_16, unit=unit) + + contact_2 = client.read_holding_registers(1252, ModbusDataType.INT_16, unit=unit) + if contact_2 != 1: + client.write_register(1252, 1, ModbusDataType.INT_16, unit=unit) + + def switch_off() -> None: + # ConsumerUsage.SUSPENDABLE_ONOFF: SG-Ready Normalbetrieb (Kontakt 1 = 0, Kontakt 2 = 0) + unit = config.configuration.modbus_id + + sg_ready_mode = client.read_holding_registers(1250, ModbusDataType.INT_16, unit=unit) + if sg_ready_mode != 1: + client.write_register(1250, 1, ModbusDataType.INT_16, unit=unit) # SG-Ready per TCP + + contact_1 = client.read_holding_registers(1251, ModbusDataType.INT_16, unit=unit) + if contact_1 != 0: + client.write_register(1251, 0, ModbusDataType.INT_16, unit=unit) + + contact_2 = client.read_holding_registers(1252, ModbusDataType.INT_16, unit=unit) + if contact_2 != 0: + client.write_register(1252, 0, ModbusDataType.INT_16, unit=unit) + return ConfigurableConsumer(consumer_config=config, initializer=initializer, error_handler=error_handler, update=update, - set_power_limit=set_limit,) + send_values=send_values, + set_power_limit=set_limit, + switch_on=switch_on, + switch_off=switch_off) -device_descriptor = DeviceDescriptor(configuration_factory=Ovum) +device_descriptor = DeviceDescriptor(configuration_factory=Ovum) \ No newline at end of file From 5d234ceb0793cf6e6ecbeeca0a218376f163653d Mon Sep 17 00:00:00 2001 From: seaspotter Date: Tue, 11 Aug 2026 22:01:44 +0200 Subject: [PATCH 10/10] flake8: fix syntax error and whitespace in consumer modules Stray pasted line broke lambda_ consumer.py syntax; idm/lambda_/ovum files had trailing whitespace on blank lines and missing EOF newlines. --- .../modules/consumers/idm/idm/consumer.py | 23 +++++++++---------- .../consumers/lambda_/lambda_/consumer.py | 20 ++++++++-------- .../modules/consumers/ovum/ovum/config.py | 11 ++++----- .../modules/consumers/ovum/ovum/consumer.py | 2 +- 4 files changed, 26 insertions(+), 30 deletions(-) diff --git a/packages/modules/consumers/idm/idm/consumer.py b/packages/modules/consumers/idm/idm/consumer.py index c2a67d0e87..d3a6b27d38 100644 --- a/packages/modules/consumers/idm/idm/consumer.py +++ b/packages/modules/consumers/idm/idm/consumer.py @@ -1,8 +1,7 @@ - #!/usr/bin/env python3 from pymodbus.constants import Endian import logging - + from modules.common.abstract_consumer import CurrentValues from modules.common.abstract_device import DeviceDescriptor from modules.common.component_state import ConsumerState @@ -11,22 +10,22 @@ from modules.common.modbus import ModbusDataType, ModbusTcpClient_ from modules.common.simcount._simcounter import SimCounterConsumer from modules.consumers.idm.idm.config import Idm - + log = logging.getLogger(__name__) - - + + def create_consumer(config: Idm): client = None sim_counter = None - + def initializer(): nonlocal client, sim_counter client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) - + def error_handler() -> None: initializer() - + def send_values(values: CurrentValues) -> None: # Sendet die aktuellen Systemwerte an die IDM Navigator 2.0-Regelung über die # Gebäudeleittechnik/Smartfox-Schnittstelle (Modbus TCP). IDM unterstützt keine @@ -35,7 +34,7 @@ def send_values(values: CurrentValues) -> None: # solange dieser Verbraucher als ConsumerUsage.SELF_CONTROLLED konfiguriert ist. # Register-Layout siehe IDM "Gebäudeleittechnik-Smartfox.pdf", Kap. 2.2.5.1 modbus_id = config.configuration.modbus_id - + # values.pv_power kommt roh aus process.py (negativ während Produktion, gleiche # Konvention wie beim EVU-Zähler) -> Vorzeichen drehen und auf 0 begrenzen, um die # reine Erzeugung zu erhalten @@ -44,7 +43,7 @@ def send_values(values: CurrentValues) -> None: # WP gemeldet würde, obwohl sie bereits von einer Fahrzeugladung verbraucht wird surplus = max(pv_power - values.home_consumption - values.cp_power, 0) battery_soc = int(values.bat_soc) if values.bat_soc is not None else -1 - + # Reg 74: Aktueller PV-Überschuss [kW] client.write_register(74, surplus / 1000, wordorder=Endian.Little, unit=modbus_id) # Reg 78: Aktuelle PV-Produktion [kW] @@ -55,7 +54,7 @@ def send_values(values: CurrentValues) -> None: client.write_register(84, values.bat_power / 1000, wordorder=Endian.Little, unit=modbus_id) # Reg 86: Batteriefüllstand [%], Default -1 (= kein Speicher) client.write_register(86, battery_soc, unit=modbus_id) - + def update() -> ConsumerState: if config.configuration.version == 1: power = client.read_holding_registers( @@ -65,7 +64,7 @@ def update() -> ConsumerState: 4122, ModbusDataType.FLOAT_32, unit=config.configuration.modbus_id) power *= 100 imported, exported = sim_counter.sim_count(power) - + return ConsumerState( power=power, imported=imported, diff --git a/packages/modules/consumers/lambda_/lambda_/consumer.py b/packages/modules/consumers/lambda_/lambda_/consumer.py index a673238a7f..d31a3f9375 100644 --- a/packages/modules/consumers/lambda_/lambda_/consumer.py +++ b/packages/modules/consumers/lambda_/lambda_/consumer.py @@ -1,9 +1,7 @@ - -Lambda consumer · PY #!/usr/bin/env python3 from pymodbus.constants import Endian import logging - + from modules.common.abstract_consumer import CurrentValues from modules.common.abstract_device import DeviceDescriptor from modules.common.component_state import ConsumerState @@ -12,22 +10,22 @@ from modules.common.modbus import ModbusDataType, ModbusTcpClient_ from modules.common.simcount._simcounter import SimCounterConsumer from modules.consumers.lambda_.lambda_.config import Lambda - + log = logging.getLogger(__name__) - - + + def create_consumer(config: Lambda): client = None sim_counter = None - + def initializer(): nonlocal client, sim_counter client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) - + def error_handler() -> None: initializer() - + def send_values(values: CurrentValues) -> None: # Sendet die aktuelle EVU-Leistung an den Lambda E-Manager (Modbus-Protokoll 1.0, # Kap. 3.2 und 4.3, Reg 102). Der E-Manager muss zusätzlich einmalig auf der @@ -35,12 +33,12 @@ def send_values(values: CurrentValues) -> None: # das kann openWB nicht per Modbus setzen. Wird vom Core automatisch aufgerufen, # solange dieser Verbraucher als ConsumerUsage.SELF_CONTROLLED konfiguriert ist. client.write_register(102, values.evu_power, wordorder=Endian.Little, unit=config.configuration.modbus_id) - + def update() -> ConsumerState: power = client.read_holding_registers( 103, ModbusDataType.INT_16, unit=config.configuration.modbus_id) imported, exported = sim_counter.sim_count(power) - + return ConsumerState( power=power, imported=imported, diff --git a/packages/modules/consumers/ovum/ovum/config.py b/packages/modules/consumers/ovum/ovum/config.py index 6c6e543f3f..d37f948158 100644 --- a/packages/modules/consumers/ovum/ovum/config.py +++ b/packages/modules/consumers/ovum/ovum/config.py @@ -1,11 +1,11 @@ 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 OvumConfiguration: def __init__(self, @@ -15,8 +15,8 @@ def __init__(self, self.ip_address = ip_address self.port = port self.modbus_id = modbus_id - - + + @auto_str class Ovum(ConsumerSetup[OvumConfiguration]): def __init__(self, @@ -34,4 +34,3 @@ def __init__(self, **kwargs) -> None: super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( ).type, configuration=configuration or OvumConfiguration(), usage=usage, **kwargs) - \ No newline at end of file diff --git a/packages/modules/consumers/ovum/ovum/consumer.py b/packages/modules/consumers/ovum/ovum/consumer.py index 67fbe9570d..48b6e9c5ce 100644 --- a/packages/modules/consumers/ovum/ovum/consumer.py +++ b/packages/modules/consumers/ovum/ovum/consumer.py @@ -124,4 +124,4 @@ def switch_off() -> None: switch_off=switch_off) -device_descriptor = DeviceDescriptor(configuration_factory=Ovum) \ No newline at end of file +device_descriptor = DeviceDescriptor(configuration_factory=Ovum)