diff --git a/packages/modules/consumers/idm/idm/config.py b/packages/modules/consumers/idm/idm/config.py
index baf3c7bcc0..e417129637 100644
--- a/packages/modules/consumers/idm/idm/config.py
+++ b/packages/modules/consumers/idm/idm/config.py
@@ -27,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 792b412626..d3a6b27d38 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 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
@@ -24,15 +26,45 @@ def initializer():
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
+ # direkte Leistungsvorgabe für die WP - die Regelung berechnet ihre Überschuss- und
+ # 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
+
+ # 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
+ 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]
+ client.write_register(78, pv_power / 1000, wordorder=Endian.Little, unit=modbus_id)
+ # Reg 82: Hausverbrauch [kW], Default 0
+ 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, 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(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)
+
return ConsumerState(
power=power,
imported=imported,
@@ -40,26 +72,19 @@ def update() -> ConsumerState:
)
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)
+ # 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)
- 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,)
+ 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 01b02a5970..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,
- sign: int = 1):
+ modbus_id: Optional[int] = 1):
self.ip_address = ip_address
self.port = port
self.modbus_id = modbus_id
- self.sign = sign
@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 83063bbb0b..d31a3f9375 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 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
@@ -24,9 +26,19 @@ def initializer():
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
+ # 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)
+
return ConsumerState(
power=power,
imported=imported,
@@ -34,8 +46,10 @@ def update() -> ConsumerState:
)
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 * config.configuration.sign, 0),
+ max(power_limit, 0),
wordorder=Endian.Little,
unit=config.configuration.modbus_id)
@@ -43,6 +57,7 @@ def set_limit(power_limit: float) -> None:
initializer=initializer,
error_handler=error_handler,
update=update,
+ send_values=send_values,
set_power_limit=set_limit,)
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..d37f948158
--- /dev/null
+++ b/packages/modules/consumers/ovum/ovum/config.py
@@ -0,0 +1,36 @@
+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):
+ self.ip_address = ip_address
+ self.port = port
+ self.modbus_id = modbus_id
+
+
+@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,
+ # 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)
diff --git a/packages/modules/consumers/ovum/ovum/consumer.py b/packages/modules/consumers/ovum/ovum/consumer.py
new file mode 100644
index 0000000000..48b6e9c5ce
--- /dev/null
+++ b/packages/modules/consumers/ovum/ovum/consumer.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+from typing import Optional
+import logging
+
+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
+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
+
+ 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:
+ # 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
+ power = client.read_holding_registers(708, 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 set_limit(power_limit: Optional[float]) -> None:
+ # 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 mode != 0:
+ client.write_register(709, 0, ModbusDataType.INT_16, unit=unit)
+ log.debug("Keine Leistungsvorgabe, Ovum-eigene PV-Watch-Regelung aktivieren.")
+ 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 mode != 2:
+ client.write_register(709, 2, ModbusDataType.INT_16, unit=unit)
+ log.debug("Leistungsvorgabe gefordert, externe Steuerung (Reg 711) aktivieren.")
+ 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,
+ send_values=send_values,
+ set_power_limit=set_limit,
+ switch_on=switch_on,
+ switch_off=switch_off)
+
+
+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)