From 2303aacc50f5fe5bca37ff1285461d78785ef42e Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Tue, 18 Aug 2026 11:46:52 +0200 Subject: [PATCH 1/4] modules --- docs/samples/sample_consumer/__init__.py | 0 .../sample_consumer/__init__.py | 0 .../sample_consumer/sample_consumer/config.py | 30 ++++ .../sample_consumer/consumer.py | 87 ++++++++++++ docs/samples/sample_consumer/vendor.py | 14 ++ packages/modules/consumers/askoma/__init__.py | 0 .../consumers/askoma/askoheat/__init__.py | 0 .../consumers/askoma/askoheat/config.py | 31 ++++ .../consumers/askoma/askoheat/consumer.py | 46 ++++++ packages/modules/consumers/askoma/vendor.py | 14 ++ packages/modules/consumers/avm/__init__.py | 0 .../modules/consumers/avm/avm/__init__.py | 0 packages/modules/consumers/avm/avm/config.py | 37 +++++ .../modules/consumers/avm/avm/consumer.py | 132 ++++++++++++++++++ packages/modules/consumers/avm/vendor.py | 14 ++ .../modules/consumers/generic/dac/__init__.py | 0 .../modules/consumers/generic/dac/config.py | 33 +++++ .../modules/consumers/generic/dac/consumer.py | 53 +++++++ .../modules/consumers/generic/dac/model.py | 8 ++ .../consumers/generic/http/__init__.py | 0 .../modules/consumers/generic/http/config.py | 49 +++++++ .../consumers/generic/http/consumer.py | 86 ++++++++++++ .../consumers/generic/json/__init__.py | 0 .../modules/consumers/generic/json/config.py | 49 +++++++ .../consumers/generic/json/consumer.py | 99 +++++++++++++ packages/modules/consumers/idm/__init__.py | 0 .../modules/consumers/idm/idm/__init__.py | 0 packages/modules/consumers/idm/idm/config.py | 34 +++++ .../modules/consumers/idm/idm/consumer.py | 90 ++++++++++++ packages/modules/consumers/idm/vendor.py | 14 ++ .../modules/consumers/lambda_/__init__.py | 0 .../consumers/lambda_/lambda_/__init__.py | 0 .../consumers/lambda_/lambda_/config.py | 32 +++++ .../consumers/lambda_/lambda_/consumer.py | 64 +++++++++ packages/modules/consumers/lambda_/vendor.py | 14 ++ packages/modules/consumers/my_pv/__init__.py | 0 .../consumers/my_pv/acthor/__init__.py | 0 .../modules/consumers/my_pv/acthor/config.py | 35 +++++ .../consumers/my_pv/acthor/consumer.py | 74 ++++++++++ .../consumers/my_pv/elwa_e/__init__.py | 0 .../modules/consumers/my_pv/elwa_e/config.py | 31 ++++ .../consumers/my_pv/elwa_e/consumer.py | 90 ++++++++++++ packages/modules/consumers/my_pv/vendor.py | 14 ++ .../modules/consumers/mystrom/__init__.py | 0 .../consumers/mystrom/mystrom/__init__.py | 0 .../consumers/mystrom/mystrom/config.py | 26 ++++ .../consumers/mystrom/mystrom/consumer.py | 55 ++++++++ packages/modules/consumers/mystrom/vendor.py | 14 ++ packages/modules/consumers/nibe/__init__.py | 0 .../consumers/nibe/nibe_s_series/__init__.py | 0 .../consumers/nibe/nibe_s_series/config.py | 28 ++++ .../consumers/nibe/nibe_s_series/consumer.py | 42 ++++++ packages/modules/consumers/nibe/vendor.py | 14 ++ packages/modules/consumers/ovum/__init__.py | 0 .../modules/consumers/ovum/ovum/__init__.py | 0 .../modules/consumers/ovum/ovum/config.py | 36 +++++ .../modules/consumers/ovum/ovum/consumer.py | 127 +++++++++++++++++ packages/modules/consumers/ovum/vendor.py | 14 ++ .../modules/consumers/ratiotherm/__init__.py | 0 .../ratiotherm/ratiotherm/__init__.py | 0 .../consumers/ratiotherm/ratiotherm/config.py | 30 ++++ .../ratiotherm/ratiotherm/consumer.py | 35 +++++ .../modules/consumers/ratiotherm/vendor.py | 14 ++ packages/modules/consumers/shelly/__init__.py | 0 .../consumers/shelly/shelly_em/__init__.py | 0 .../consumers/shelly/shelly_em/config.py | 34 +++++ .../consumers/shelly/shelly_em/consumer.py | 46 ++++++ .../consumers/shelly/shelly_pm/__init__.py | 0 .../consumers/shelly/shelly_pm/config.py | 37 +++++ .../consumers/shelly/shelly_pm/consumer.py | 82 +++++++++++ packages/modules/consumers/shelly/vendor.py | 14 ++ packages/modules/consumers/solvis/__init__.py | 0 .../consumers/solvis/solvis/__init__.py | 0 .../modules/consumers/solvis/solvis/config.py | 27 ++++ .../consumers/solvis/solvis/consumer.py | 38 +++++ packages/modules/consumers/solvis/vendor.py | 14 ++ .../modules/consumers/stiebel/__init__.py | 0 .../consumers/stiebel/stiebel/__init__.py | 0 .../consumers/stiebel/stiebel/config.py | 30 ++++ .../consumers/stiebel/stiebel/consumer.py | 39 ++++++ packages/modules/consumers/stiebel/vendor.py | 14 ++ .../modules/consumers/viessmann/__init__.py | 0 .../modules/consumers/viessmann/vendor.py | 14 ++ .../viessmann/viessmann_heat_pump/__init__.py | 0 .../viessmann/viessmann_heat_pump/config.py | 27 ++++ .../viessmann/viessmann_heat_pump/consumer.py | 31 ++++ 86 files changed, 2156 insertions(+) create mode 100644 docs/samples/sample_consumer/__init__.py create mode 100644 docs/samples/sample_consumer/sample_consumer/__init__.py create mode 100644 docs/samples/sample_consumer/sample_consumer/config.py create mode 100644 docs/samples/sample_consumer/sample_consumer/consumer.py create mode 100644 docs/samples/sample_consumer/vendor.py create mode 100644 packages/modules/consumers/askoma/__init__.py create mode 100644 packages/modules/consumers/askoma/askoheat/__init__.py create mode 100644 packages/modules/consumers/askoma/askoheat/config.py create mode 100644 packages/modules/consumers/askoma/askoheat/consumer.py create mode 100644 packages/modules/consumers/askoma/vendor.py create mode 100644 packages/modules/consumers/avm/__init__.py create mode 100644 packages/modules/consumers/avm/avm/__init__.py create mode 100644 packages/modules/consumers/avm/avm/config.py create mode 100644 packages/modules/consumers/avm/avm/consumer.py create mode 100644 packages/modules/consumers/avm/vendor.py create mode 100644 packages/modules/consumers/generic/dac/__init__.py create mode 100644 packages/modules/consumers/generic/dac/config.py create mode 100644 packages/modules/consumers/generic/dac/consumer.py create mode 100644 packages/modules/consumers/generic/dac/model.py create mode 100644 packages/modules/consumers/generic/http/__init__.py create mode 100644 packages/modules/consumers/generic/http/config.py create mode 100644 packages/modules/consumers/generic/http/consumer.py create mode 100644 packages/modules/consumers/generic/json/__init__.py create mode 100644 packages/modules/consumers/generic/json/config.py create mode 100644 packages/modules/consumers/generic/json/consumer.py create mode 100644 packages/modules/consumers/idm/__init__.py create mode 100644 packages/modules/consumers/idm/idm/__init__.py create mode 100644 packages/modules/consumers/idm/idm/config.py create mode 100644 packages/modules/consumers/idm/idm/consumer.py create mode 100644 packages/modules/consumers/idm/vendor.py create mode 100644 packages/modules/consumers/lambda_/__init__.py create mode 100644 packages/modules/consumers/lambda_/lambda_/__init__.py create mode 100644 packages/modules/consumers/lambda_/lambda_/config.py create mode 100644 packages/modules/consumers/lambda_/lambda_/consumer.py create mode 100644 packages/modules/consumers/lambda_/vendor.py create mode 100644 packages/modules/consumers/my_pv/__init__.py create mode 100644 packages/modules/consumers/my_pv/acthor/__init__.py create mode 100644 packages/modules/consumers/my_pv/acthor/config.py create mode 100644 packages/modules/consumers/my_pv/acthor/consumer.py create mode 100644 packages/modules/consumers/my_pv/elwa_e/__init__.py create mode 100644 packages/modules/consumers/my_pv/elwa_e/config.py create mode 100644 packages/modules/consumers/my_pv/elwa_e/consumer.py create mode 100644 packages/modules/consumers/my_pv/vendor.py create mode 100644 packages/modules/consumers/mystrom/__init__.py create mode 100644 packages/modules/consumers/mystrom/mystrom/__init__.py create mode 100644 packages/modules/consumers/mystrom/mystrom/config.py create mode 100644 packages/modules/consumers/mystrom/mystrom/consumer.py create mode 100644 packages/modules/consumers/mystrom/vendor.py create mode 100644 packages/modules/consumers/nibe/__init__.py create mode 100644 packages/modules/consumers/nibe/nibe_s_series/__init__.py create mode 100644 packages/modules/consumers/nibe/nibe_s_series/config.py create mode 100644 packages/modules/consumers/nibe/nibe_s_series/consumer.py create mode 100644 packages/modules/consumers/nibe/vendor.py 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 create mode 100644 packages/modules/consumers/ratiotherm/__init__.py create mode 100644 packages/modules/consumers/ratiotherm/ratiotherm/__init__.py create mode 100644 packages/modules/consumers/ratiotherm/ratiotherm/config.py create mode 100644 packages/modules/consumers/ratiotherm/ratiotherm/consumer.py create mode 100644 packages/modules/consumers/ratiotherm/vendor.py create mode 100644 packages/modules/consumers/shelly/__init__.py create mode 100644 packages/modules/consumers/shelly/shelly_em/__init__.py create mode 100644 packages/modules/consumers/shelly/shelly_em/config.py create mode 100644 packages/modules/consumers/shelly/shelly_em/consumer.py create mode 100644 packages/modules/consumers/shelly/shelly_pm/__init__.py create mode 100644 packages/modules/consumers/shelly/shelly_pm/config.py create mode 100644 packages/modules/consumers/shelly/shelly_pm/consumer.py create mode 100644 packages/modules/consumers/shelly/vendor.py create mode 100644 packages/modules/consumers/solvis/__init__.py create mode 100644 packages/modules/consumers/solvis/solvis/__init__.py create mode 100644 packages/modules/consumers/solvis/solvis/config.py create mode 100644 packages/modules/consumers/solvis/solvis/consumer.py create mode 100644 packages/modules/consumers/solvis/vendor.py create mode 100644 packages/modules/consumers/stiebel/__init__.py create mode 100644 packages/modules/consumers/stiebel/stiebel/__init__.py create mode 100644 packages/modules/consumers/stiebel/stiebel/config.py create mode 100644 packages/modules/consumers/stiebel/stiebel/consumer.py create mode 100644 packages/modules/consumers/stiebel/vendor.py create mode 100644 packages/modules/consumers/viessmann/__init__.py create mode 100644 packages/modules/consumers/viessmann/vendor.py create mode 100644 packages/modules/consumers/viessmann/viessmann_heat_pump/__init__.py create mode 100644 packages/modules/consumers/viessmann/viessmann_heat_pump/config.py create mode 100644 packages/modules/consumers/viessmann/viessmann_heat_pump/consumer.py diff --git a/docs/samples/sample_consumer/__init__.py b/docs/samples/sample_consumer/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/samples/sample_consumer/sample_consumer/__init__.py b/docs/samples/sample_consumer/sample_consumer/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/samples/sample_consumer/sample_consumer/config.py b/docs/samples/sample_consumer/sample_consumer/config.py new file mode 100644 index 0000000000..e39d7c6533 --- /dev/null +++ b/docs/samples/sample_consumer/sample_consumer/config.py @@ -0,0 +1,30 @@ +from typing import List, Optional + +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 SampleConsumerConfiguration: + 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 SampleConsumer(ConsumerSetup[SampleConsumerConfiguration]): + def __init__(self, + name: str = "Sample Consumer", + type: str = "sample_consumer", + id: int = 0, + configuration: SampleConsumerConfiguration = None, + usage: List[ConsumerUsage] = [ConsumerUsage.SUSPENDABLE_TUNABLE, + ConsumerUsage.METER_ONLY]) -> None: + super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( + ).type, configuration=configuration or SampleConsumerConfiguration(), usage=usage) diff --git a/docs/samples/sample_consumer/sample_consumer/consumer.py b/docs/samples/sample_consumer/sample_consumer/consumer.py new file mode 100644 index 0000000000..af25c43365 --- /dev/null +++ b/docs/samples/sample_consumer/sample_consumer/consumer.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +from enum import IntEnum +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.configurable_consumer import ConfigurableConsumer +from modules.common.modbus import ModbusDataType, ModbusTcpClient_ +from modules.common.simcount._simcounter import SimCounterConsumer +from modules.consumers.askoma.askoheat.config import SampleConsumer + +log = logging.getLogger(__name__) + + +class Register(IntEnum): + POWER = 1000 + TEMP0 = 1001 + TEMP1 = 1030 + TEMP2 = 1031 + STATUS = 1003 + + +REG_MAPPING = ( + (Register.POWER, [ModbusDataType.INT_16]), + (Register.TEMP0, [ModbusDataType.INT_16]), + (Register.TEMP1, [ModbusDataType.INT_16]), + (Register.TEMP2, [ModbusDataType.INT_16]), + (Register.STATUS, ModbusDataType.INT_16), +) + + +def create_consumer(config: SampleConsumer): + 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 update() -> ConsumerState: + nonlocal client, sim_counter + resp = client.read_holding_registers_bulk( + Register.POWER, 35, mapping=REG_MAPPING, unit=config.configuration.modbus_id) + imported, exported = sim_counter.sim_count(resp[Register.POWER]) + return ConsumerState( + power=resp[Register.POWER], + imported=imported, + exported=exported, + temperatures=[resp[Register.TEMP0]/10, resp[Register.TEMP1]/10, resp[Register.TEMP2]/10] + ) + + # ConsumerUsage.SUSPENDABLE_TUNABLE + def set_limit(power_limit: float) -> None: + nonlocal client + client.write_registers(1000, power_limit, unit=config.configuration.modbus_id) + + # ODER + # ConsumerUsage.SUSPENDABLE_ONOFF + # ConsumerUsage.CONTINUOUS + def switch_on() -> None: + nonlocal client + client.write_registers(1000, 1, unit=config.configuration.modbus_id) + + def switch_off() -> None: + nonlocal client + client.write_registers(1000, 0, unit=config.configuration.modbus_id) + + # ODER + # ConsumerUsage.SELF_CONTROLLED + def send_values(values: CurrentValues) -> None: + client.write_registers(1000, values.evu_power, unit=config.configuration.modbus_id) + + 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=SampleConsumer) diff --git a/docs/samples/sample_consumer/vendor.py b/docs/samples/sample_consumer/vendor.py new file mode 100644 index 0000000000..9741383ef5 --- /dev/null +++ b/docs/samples/sample_consumer/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 = "Sample" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) diff --git a/packages/modules/consumers/askoma/__init__.py b/packages/modules/consumers/askoma/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/askoma/askoheat/__init__.py b/packages/modules/consumers/askoma/askoheat/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/askoma/askoheat/config.py b/packages/modules/consumers/askoma/askoheat/config.py new file mode 100644 index 0000000000..2559a81dc8 --- /dev/null +++ b/packages/modules/consumers/askoma/askoheat/config.py @@ -0,0 +1,31 @@ +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 AskoheatConfiguration: + 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 Askoheat(ConsumerSetup[AskoheatConfiguration]): + def __init__(self, + name: str = "Askoheat+", + type: str = "askoheat", + id: int = 0, + configuration: AskoheatConfiguration = 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 AskoheatConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/askoma/askoheat/consumer.py b/packages/modules/consumers/askoma/askoheat/consumer.py new file mode 100644 index 0000000000..71d9728a2b --- /dev/null +++ b/packages/modules/consumers/askoma/askoheat/consumer.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +import logging +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.askoma.askoheat.config import Askoheat + +log = logging.getLogger(__name__) + + +def create_consumer(config: Askoheat): + 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 update() -> ConsumerState: + power = client.read_input_registers(110, ModbusDataType.INT_16, unit=config.configuration.modbus_id) + imported, exported = sim_counter.sim_count(power) + return ConsumerState( + power=power, + imported=imported, + exported=exported, + temperatures=[client.read_input_registers(638, ModbusDataType.INT_16, unit=config.configuration.modbus_id)] + ) + + def set_limit(power_limit: float) -> None: + client.write_register(201, power_limit, unit=config.configuration.modbus_id) + + return ConfigurableConsumer(consumer_config=config, + initializer=initializer, + error_handler=error_handler, + update=update, + set_power_limit=set_limit,) + + +device_descriptor = DeviceDescriptor(configuration_factory=Askoheat) diff --git a/packages/modules/consumers/askoma/vendor.py b/packages/modules/consumers/askoma/vendor.py new file mode 100644 index 0000000000..b1861e388d --- /dev/null +++ b/packages/modules/consumers/askoma/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 = "ASKOMA" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) diff --git a/packages/modules/consumers/avm/__init__.py b/packages/modules/consumers/avm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/avm/avm/__init__.py b/packages/modules/consumers/avm/avm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/avm/avm/config.py b/packages/modules/consumers/avm/avm/config.py new file mode 100644 index 0000000000..40c6998564 --- /dev/null +++ b/packages/modules/consumers/avm/avm/config.py @@ -0,0 +1,37 @@ +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 AvmConfiguration: + def __init__(self, + ip_address: Optional[str] = None, + username: Optional[str] = None, + password: Optional[str] = None, + session_id: Optional[str] = None, + session_mtime: Optional[str] = None, + name: Optional[str] = None) -> None: + self.ip_address = ip_address + self.username = username + self.password = password + self.session_id = session_id # don't show in UI + self.session_mtime = session_mtime # don't show in UI + self.name = name + + +@auto_str +class Avm(ConsumerSetup[AvmConfiguration]): + def __init__(self, + name: str = "AVM Fritz!Box", + type: str = "avm", + id: int = 0, + configuration: AvmConfiguration = 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 AvmConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/avm/avm/consumer.py b/packages/modules/consumers/avm/avm/consumer.py new file mode 100644 index 0000000000..5b28d073c3 --- /dev/null +++ b/packages/modules/consumers/avm/avm/consumer.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +import hashlib +import logging +import time +import xml.etree.ElementTree as ET + +from dataclass_utils._dataclass_asdict import asdict +from helpermodules.pub import Pub +from modules.common import req +from modules.common.abstract_device import DeviceDescriptor +from modules.common.component_state import ConsumerState +from modules.common.configurable_consumer import ConfigurableConsumer +from modules.consumers.avm.avm.config import Avm + +log = logging.getLogger(__name__) + +INVALID_SESSIONID = "0000000000000000" + + +def create_consumer(config: Avm): + ain = None # Actuator Identification Number + + def update() -> ConsumerState: + nonlocal ain + ensure_valid_session_id() + response = req.get_http_session().get( + f"http://{config.configuration.ip_address}/webservices/homeautoswitch.lua?sid=" + f"{config.configuration.session_id}&switchcmd=getdevicelistinfos") + deviceListElementTree = ET.fromstring(response.text.strip()) + + for device in deviceListElementTree: + name = device.find("name").text + if name == config.configuration.name: + presentText = device.find("present").text + if presentText != '1': + continue + + ain = device.attrib["identifier"] + powermeterBlock = device.find("powermeter") + if powermeterBlock is not None: + # AVM returns mW, convert to W here + power = float(powermeterBlock.find("power").text)/1000 + # AVM returns mV, convert to V here + voltageInfo = powermeterBlock.find("voltage") + if voltageInfo is not None: + voltages = [float(voltageInfo.text)/1000, 0, 0] + # AVM returns Wh + imported = float(powermeterBlock.find("energy").text) + temperatureBlock = device.find("temperature") + if temperatureBlock is not None: + # AVM returns tenths of degrees Celsius + temperature = float(temperatureBlock.find("celsius").text)/10.0 + switchBlock = device.find("switch") + if switchBlock is not None: + state = (int(switchBlock.find("state").text) == 1) + + return ConsumerState( + power=power, + imported=imported, + temperatures=[temperature], + state=state, + voltages=voltages + ) + + def ensure_valid_session_id(): + if check_valid_session_id() is False: + config.configuration.session_id = get_session_id() + config.configuration.session_mtime = time.time() + Pub().pub(f"openWB/set/system/device/{config.id}/config", asdict(config)) + + def check_valid_session_id() -> bool: + return (config.configuration.session_id is None or + config.configuration.session_mtime is None or + time.time() - config.configuration.session_mtime > 300) + + def get_session_id(): + # checking existing sessionID + data = { + 'content-type': 'application/text' + } + response = req.get_http_session().post( + f"http://{config.configuration.ip_address}/login_sid.lua", data=data, timeout=5) + challengeResponse = ET.fromstring(response.content) + session_id = challengeResponse.find('SID').text + if session_id != INVALID_SESSIONID: + return + blockTimeXML = challengeResponse.find('BlockTime') + if blockTimeXML is not None and int(blockTimeXML.text) > 0: + raise Exception("Durch Anmeldefehler in der Vergangenheit ist der Zugang zur FRITZ!Box " + f"noch für {blockTimeXML.text} Sekunden gesperrt.") + + # last sessionID was invalid, performing new challenge-response authentication + challenge = challengeResponse.find('Challenge').text + m = hashlib.md5() + m.update((f"{challenge}-{config.configuration.password}").encode('utf-16le')) + hashedPassword = m.hexdigest() + + data = { + 'username': config.configuration.username, + 'response': challenge + "-" + hashedPassword + } + try: + response = req.get_http_session().post( + f"http://{config.configuration.ip_address}/login_sid.lua", data=data, timeout=5) + session_info = ET.fromstring(response.content) + session_id = session_info.find('SID').text + except Exception: + session_id = None + raise Exception("Anmeldung fehlgeschlagen, bitte Benutzername und Passwort überprüfen. Anmeldung für " + f"die nächsten {session_info.find('BlockTime').text} Sekunden durch FRITZ!Box-Webinterface " + "gesperrt.") + return session_id + + def switch_on() -> None: + ensure_valid_session_id() + req.get_http_session().get( + f"http://{config.configuration.ip_address}/webservices/homeautoswitch.lua?sid=" + f"{config.configuration.session_id}&switchcmd=setswitchon&ain={ain}") + + def switch_off() -> None: + ensure_valid_session_id() + req.get_http_session().get( + f"http://{config.configuration.ip_address}/webservices/homeautoswitch.lua?sid=" + f"{config.configuration.session_id}&switchcmd=setswitchoff&ain={ain}") + + return ConfigurableConsumer(consumer_config=config, + update=update, + switch_on=switch_on, + switch_off=switch_off) + + +device_descriptor = DeviceDescriptor(configuration_factory=Avm) diff --git a/packages/modules/consumers/avm/vendor.py b/packages/modules/consumers/avm/vendor.py new file mode 100644 index 0000000000..de4025faae --- /dev/null +++ b/packages/modules/consumers/avm/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 = "AVM Fritz!Box" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) diff --git a/packages/modules/consumers/generic/dac/__init__.py b/packages/modules/consumers/generic/dac/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/generic/dac/config.py b/packages/modules/consumers/generic/dac/config.py new file mode 100644 index 0000000000..ac9e34cd40 --- /dev/null +++ b/packages/modules/consumers/generic/dac/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 modules.consumers.generic.dac.model import Model +from ..vendor import vendor_descriptor + + +@auto_str +class DacConfiguration: + def __init__(self, + ip_address: Optional[str] = None, + port: Optional[int] = 502, + modbus_id: Optional[int] = 1, + model: Model = Model.N4Dac02): + self.ip_address = ip_address + self.port = port + self.modbus_id = modbus_id + self.model = model + + +@auto_str +class Dac(ConsumerSetup[DacConfiguration]): + def __init__(self, + name: str = "Digital-Analog-Konverter (DAC) 0.01V bis 10.0V", + type: str = "dac", + id: int = 0, + configuration: DacConfiguration = None, + usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_TUNABLE,), + **kwargs) -> None: + super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( + ).type, configuration=configuration or DacConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/generic/dac/consumer.py b/packages/modules/consumers/generic/dac/consumer.py new file mode 100644 index 0000000000..654f2ab1c5 --- /dev/null +++ b/packages/modules/consumers/generic/dac/consumer.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +import logging +from modules.common.abstract_device import DeviceDescriptor +from modules.common.component_type import ComponentType +from modules.common.configurable_consumer import ConfigurableConsumer +from modules.common.modbus import ModbusTcpClient_ +from modules.common.simcount._simcounter import SimCounterConsumer +from modules.consumers.generic.dac.config import Dac +from modules.consumers.generic.dac.model import Model + +log = logging.getLogger(__name__) + + +def create_consumer(config: Dac): + 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 set_limit(power_limit: float) -> None: + if config.configuration.model == Model.N4Dac02 or config.configuration.model == Model.M120T: + # pwoer_limit -= power + pass + + # power_limit = max(power_limit, 0) + # if config.configuration.model == Model.N4Dac02: + # client.write_register(1, power_limit * 1000 / max_power, ModbusDataType.INT_16, + # unit=config.configuration.modbus_id) + # elif config.configuration.model == Model.DA02: + # client.write_register(0x01f4, power_limit * 4000 / max_power, + # ModbusDataType.INT_16, unit=config.configuration.modbus_id) + # elif config.configuration.model == Model.M120T: + # # ausgabe nicht kleiner 0,9V sonst Leistungsregelung der WP aus + # power_limit = max(power_limit * 4095 / max_power, 370) + # client.write_register(0x01f4, power_limit, ModbusDataType.INT_16, unit=config.configuration.modbus_id) + # elif config.configuration.model == Model.AA02B: + # power_limit = max((power_limit * (4095-820) / max_power)+820, 820) + # # ausgabe nicht kleiner 4ma sonst Leistungsregelung der WP aus + # client.write_register(0x01f4, power_limit, ModbusDataType.INT_16, unit=config.configuration.modbus_id) + + return ConfigurableConsumer(consumer_config=config, + initializer=initializer, + error_handler=error_handler, + set_power_limit=set_limit,) + + +device_descriptor = DeviceDescriptor(configuration_factory=Dac) diff --git a/packages/modules/consumers/generic/dac/model.py b/packages/modules/consumers/generic/dac/model.py new file mode 100644 index 0000000000..364c712555 --- /dev/null +++ b/packages/modules/consumers/generic/dac/model.py @@ -0,0 +1,8 @@ +from enum import Enum + + +class Model(Enum): + N4Dac02 = "N4Dac02" + DA02 = "DA02" + M120T = "M120T" + AA02B = "AA02B" diff --git a/packages/modules/consumers/generic/http/__init__.py b/packages/modules/consumers/generic/http/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/generic/http/config.py b/packages/modules/consumers/generic/http/config.py new file mode 100644 index 0000000000..6fe3eef212 --- /dev/null +++ b/packages/modules/consumers/generic/http/config.py @@ -0,0 +1,49 @@ +from typing import 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 HttpConfiguration: + def __init__(self, + url: str = None, + current_l1_path: str = None, + current_l2_path: str = None, + current_l3_path: str = None, + power_path: str = None, + temperatures_path: str = None, + imported_path: str = None, + exported_path: str = None, + switch_on_path: str = None, + switch_off_path: str = None, + set_power_limit_path: str = None): + self.url = url + self.current_l1_path = current_l1_path + self.current_l2_path = current_l2_path + self.current_l3_path = current_l3_path + self.power_path = power_path + self.temperatures_path = temperatures_path + self.imported_path = imported_path + self.exported_path = exported_path + self.switch_on_path = switch_on_path + self.switch_off_path = switch_off_path + self.set_power_limit_path = set_power_limit_path + + +@auto_str +class Http(ConsumerSetup[HttpConfiguration]): + def __init__(self, + name: str = "HTTP-Verbraucher", + type: str = "http", + id: int = 0, + configuration: HttpConfiguration = None, + usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.METER_ONLY, + ConsumerUsage.CONTINUOUS, + ConsumerUsage.SUSPENDABLE_ONOFF, + ConsumerUsage.SUSPENDABLE_TUNABLE), + **kwargs) -> None: + super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( + ).type, configuration=configuration or HttpConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/generic/http/consumer.py b/packages/modules/consumers/generic/http/consumer.py new file mode 100644 index 0000000000..080e4983ad --- /dev/null +++ b/packages/modules/consumers/generic/http/consumer.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +import logging + +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 +from modules.common.simcount._simcounter import SimCounterConsumer +from modules.consumers.generic.http.config import Http +from modules.devices.generic.http.api import (create_post_function, create_request_function, + create_request_function_array) +log = logging.getLogger(__name__) + + +def create_consumer(config: Http): + session = None + sim_counter = None + get_power = None + get_imported = None + get_exported = None + get_currents = None + get_temperatures = None + post_set_power_limit = None + post_switch_on = None + post_switch_off = None + + def initializer(): + nonlocal session, sim_counter + nonlocal get_power, get_imported, get_exported, get_currents, get_temperatures + nonlocal post_set_power_limit, post_switch_on, post_switch_off + if not config.configuration.url.startswith('https://'): + raise ValueError("Only HTTPS URLs allowed for security") + session = req.get_http_session() + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + + get_power = create_request_function(config.configuration.url, config.configuration.power_path) + get_imported = create_request_function(config.configuration.url, config.configuration.imported_path) + get_exported = create_request_function(config.configuration.url, config.configuration.exported_path) + get_currents = create_request_function_array(config.configuration.url, [ + config.configuration.current_l1_path, + config.configuration.current_l2_path, + config.configuration.current_l3_path, + ]) + get_temperatures = create_request_function(config.configuration.url, config.configuration.temperatures_path) + post_set_power_limit = create_post_function(config.configuration.url, config.configuration.set_power_limit_path) + post_switch_on = create_post_function(config.configuration.url, config.configuration.switch_on_path) + post_switch_off = create_post_function(config.configuration.url, config.configuration.switch_off_path) + + def update() -> None: + power = get_power(session) + exported = get_exported(session) + imported = get_imported(session) + currents = get_currents(session) + temperatures = get_temperatures(session) + if imported is None or exported is None: + imported, exported = sim_counter.sim_count(power) + return ConsumerState( + power=power, + currents=currents, + imported=imported, + exported=exported, + temperatures=temperatures if isinstance(temperatures, list) else [temperatures], + ) + + def switch_on(): + # Authorization? + post_switch_on(session, params={"state": True}) + + def switch_off(): + # Authorization? + post_switch_off(session, params={"state": False}) + + def set_power_limit(power_limit: int): + # Authorization? + post_set_power_limit(session, params={"power_limit": power_limit}) + + 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=Http) diff --git a/packages/modules/consumers/generic/json/__init__.py b/packages/modules/consumers/generic/json/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/generic/json/config.py b/packages/modules/consumers/generic/json/config.py new file mode 100644 index 0000000000..fda50af220 --- /dev/null +++ b/packages/modules/consumers/generic/json/config.py @@ -0,0 +1,49 @@ +from typing import 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 JsonConfiguration: + def __init__(self, + url: str = None, + jq_current_l1: str = None, + jq_current_l2: str = None, + jq_current_l3: str = None, + jq_power: str = None, + jq_temperatures: str = None, + jq_imported: str = None, + jq_exported: str = None, + jq_switch_on: str = None, + jq_switch_off: str = None, + jq_set_power_limit: str = None): + self.url = url + self.jq_current_l1 = jq_current_l1 + self.jq_current_l2 = jq_current_l2 + self.jq_current_l3 = jq_current_l3 + self.jq_power = jq_power + self.jq_temperatures = jq_temperatures + self.jq_imported = jq_imported + self.jq_exported = jq_exported + self.jq_switch_on = jq_switch_on + self.jq_switch_off = jq_switch_off + self.jq_set_power_limit = jq_set_power_limit + + +@auto_str +class Json(ConsumerSetup[JsonConfiguration]): + def __init__(self, + name: str = "JSON-Verbraucher", + type: str = "json", + id: int = 0, + configuration: JsonConfiguration = None, + usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.METER_ONLY, + ConsumerUsage.CONTINUOUS, + ConsumerUsage.SUSPENDABLE_ONOFF, + ConsumerUsage.SUSPENDABLE_TUNABLE), + **kwargs) -> None: + super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( + ).type, configuration=configuration or JsonConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/generic/json/consumer.py b/packages/modules/consumers/generic/json/consumer.py new file mode 100644 index 0000000000..07f0e070d3 --- /dev/null +++ b/packages/modules/consumers/generic/json/consumer.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +import logging +from typing import Callable, Optional +import jq + +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 +from modules.common.simcount._simcounter import SimCounterConsumer +from modules.consumers.generic.json.config import Json + +log = logging.getLogger(__name__) + + +def create_consumer(config: Json): + session = None + sim_counter = None + jq_power = None + jq_imported = None + jq_exported = None + jq_currents = None + jq_temperatures = None + jq_set_power_limit = None + jq_switch_on = None + jq_switch_off = None + + def _compile_jq_filters() -> None: + nonlocal jq_power, jq_imported, jq_exported, jq_currents, jq_temperatures + jq_power = jq.compile(config.configuration.jq_power) + jq_currents = [jq.compile(c) for c in config.configuration.jq_currents] if all( + config.configuration.jq_currents) else None + jq_imported = jq.compile(config.configuration.jq_imported) if config.configuration.jq_imported else None + jq_exported = jq.compile(config.configuration.jq_exported) if config.configuration.jq_exported else None + jq_temperatures = jq.compile( + config.configuration.jq_temperatures) if config.configuration.jq_temperatures else None + + def create_post_function(path: Optional[str]) -> Callable[[dict], None]: + if path is None: + return lambda _: None + else: + def post_function(params: dict): + session.post(config.configuration.url + path, json=params, timeout=5) + return post_function + + def initializer(): + nonlocal session, sim_counter + nonlocal jq_set_power_limit, jq_switch_on, jq_switch_off + if not config.configuration.url.startswith('https://'): + raise ValueError("Only HTTPS URLs allowed for security") + session = req.get_http_session() + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + + _compile_jq_filters() + jq_set_power_limit = create_post_function(config.configuration.jq_set_power_limit) + jq_switch_on = create_post_function(config.configuration.jq_switch_on) + jq_switch_off = create_post_function(config.configuration.jq_switch_off) + + def update() -> None: + response = req.get_http_session().get(config.configuration.url, timeout=5).json() + power = float(jq_power.input(response).first()) + temperatures = float(jq_temperatures.input(response).first()) if jq_temperatures is not None else None + currents = ([float(j.input(response).first()) for j in jq_currents] if jq_currents is not None else None) + if jq_imported is None or jq_exported is None: + imported, exported = sim_counter.sim_count(power) + else: + imported = float(jq_imported.input(response).first()) + exported = float(jq_exported.input(response).first()) + + return ConsumerState( + power=power, + currents=currents, + imported=imported, + exported=exported, + temperatures=temperatures if isinstance(temperatures, list) else [temperatures], + ) + + def switch_on(): + # Authorization? + jq_switch_on(session, params={"state": True}) + + def switch_off(): + # Authorization? + jq_switch_off(session, params={"state": False}) + + def set_power_limit(power_limit: int): + # Authorization? + jq_set_power_limit(session, params={"power_limit": power_limit}) + + 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=Json) diff --git a/packages/modules/consumers/idm/__init__.py b/packages/modules/consumers/idm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/idm/idm/__init__.py b/packages/modules/consumers/idm/idm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/idm/idm/config.py b/packages/modules/consumers/idm/idm/config.py new file mode 100644 index 0000000000..e417129637 --- /dev/null +++ b/packages/modules/consumers/idm/idm/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 IdmConfiguration: + def __init__(self, + ip_address: Optional[str] = None, + port: Optional[int] = 502, + modbus_id: Optional[int] = 1, + version: int = 1): + self.ip_address = ip_address + self.port = port + self.modbus_id = modbus_id + self.version = version + + +@auto_str +class Idm(ConsumerSetup[IdmConfiguration]): + def __init__(self, + name: str = "Wärmepumpe der Firma IDM mit Navigatorregelung 1.7/2.0", + type: str = "idm", + 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( + ).type, configuration=configuration or IdmConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/idm/idm/consumer.py b/packages/modules/consumers/idm/idm/consumer.py new file mode 100644 index 0000000000..d3a6b27d38 --- /dev/null +++ b/packages/modules/consumers/idm/idm/consumer.py @@ -0,0 +1,90 @@ +#!/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 +from modules.common.configurable_consumer import ConfigurableConsumer +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 + # 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) + else: + 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, + 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, + send_values=send_values, + set_power_limit=set_limit) + + +device_descriptor = DeviceDescriptor(configuration_factory=Idm) diff --git a/packages/modules/consumers/idm/vendor.py b/packages/modules/consumers/idm/vendor.py new file mode 100644 index 0000000000..e2d6437048 --- /dev/null +++ b/packages/modules/consumers/idm/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 = "IDM" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) diff --git a/packages/modules/consumers/lambda_/__init__.py b/packages/modules/consumers/lambda_/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/lambda_/lambda_/__init__.py b/packages/modules/consumers/lambda_/lambda_/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/lambda_/lambda_/config.py b/packages/modules/consumers/lambda_/lambda_/config.py new file mode 100644 index 0000000000..c02249395f --- /dev/null +++ b/packages/modules/consumers/lambda_/lambda_/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 LambdaConfiguration: + 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 Lambda(ConsumerSetup[LambdaConfiguration]): + def __init__(self, + name: str = "Lambda Wärmepumpe", + type: str = "lambda_", + 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( + ).type, configuration=configuration or LambdaConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/lambda_/lambda_/consumer.py b/packages/modules/consumers/lambda_/lambda_/consumer.py new file mode 100644 index 0000000000..d31a3f9375 --- /dev/null +++ b/packages/modules/consumers/lambda_/lambda_/consumer.py @@ -0,0 +1,64 @@ +#!/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 +from modules.common.configurable_consumer import ConfigurableConsumer +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 + # 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) + imported, exported = sim_counter.sim_count(power) + + 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, + send_values=send_values, + set_power_limit=set_limit,) + + +device_descriptor = DeviceDescriptor(configuration_factory=Lambda) diff --git a/packages/modules/consumers/lambda_/vendor.py b/packages/modules/consumers/lambda_/vendor.py new file mode 100644 index 0000000000..4cef2bafab --- /dev/null +++ b/packages/modules/consumers/lambda_/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 = "Lambda" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) diff --git a/packages/modules/consumers/my_pv/__init__.py b/packages/modules/consumers/my_pv/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/my_pv/acthor/__init__.py b/packages/modules/consumers/my_pv/acthor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/my_pv/acthor/config.py b/packages/modules/consumers/my_pv/acthor/config.py new file mode 100644 index 0000000000..2e8f785058 --- /dev/null +++ b/packages/modules/consumers/my_pv/acthor/config.py @@ -0,0 +1,35 @@ +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 ActhorConfiguration: + def __init__(self, + ip_address: Optional[str] = None, + port: Optional[int] = 502, + modbus_id: Optional[int] = 1, + model: Optional[str] = "9s45", + max_power: Optional[int] = 1000): + self.ip_address = ip_address + self.port = port + self.modbus_id = modbus_id + self.model = model + self.max_power = max_power + + +@auto_str +class Acthor(ConsumerSetup[ActhorConfiguration]): + def __init__(self, + name: str = "my-PV Acthor oder Elwa2 Heizstab", + type: str = "acthor", + id: int = 0, + configuration: ActhorConfiguration = 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 ActhorConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/my_pv/acthor/consumer.py b/packages/modules/consumers/my_pv/acthor/consumer.py new file mode 100644 index 0000000000..f35b96d8dc --- /dev/null +++ b/packages/modules/consumers/my_pv/acthor/consumer.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +from enum import IntEnum +import logging +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.my_pv.acthor.config import Acthor + +log = logging.getLogger(__name__) + + +class Register(IntEnum): + POWER = 1000 + TEMP0 = 1001 + TEMP1 = 1030 + TEMP2 = 1031 + STATUS = 1003 + + +FACTORS = {"9s45": 45000, + "9s27": 27000, + "9s18": 18000, + "9s": 9000, + "M3": 6000, + "E2M1": 3500, + "E2M3": 6500, + "M1": 3000} +REG_MAPPING = ( + (Register.POWER, [ModbusDataType.INT_16]), + (Register.TEMP0, [ModbusDataType.INT_16]), + (Register.TEMP1, [ModbusDataType.INT_16]), + (Register.TEMP2, [ModbusDataType.INT_16]), + (Register.STATUS, ModbusDataType.INT_16), +) + + +def create_consumer(config: Acthor): + 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 update() -> ConsumerState: + resp = client.read_holding_registers_bulk( + Register.POWER, 35, mapping=REG_MAPPING, unit=config.configuration.modbus_id) + power = resp[Register.POWER] + imported, exported = sim_counter.sim_count(power) + return ConsumerState( + power=power, + imported=imported, + exported=exported, + temperatures=[resp[Register.TEMP0]/10, resp[Register.TEMP1]/10, resp[Register.TEMP2]/10] + ) + + def set_limit(power_limit: float) -> None: + power_limit = power_limit * FACTORS.get(config.configuration.model, 9000)/config.configuration.max_power + client.write_registers(1000, power_limit, unit=config.configuration.modbus_id) + return ConfigurableConsumer(consumer_config=config, + initializer=initializer, + error_handler=error_handler, + update=update, + set_power_limit=set_limit,) + + +device_descriptor = DeviceDescriptor(configuration_factory=Acthor) diff --git a/packages/modules/consumers/my_pv/elwa_e/__init__.py b/packages/modules/consumers/my_pv/elwa_e/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/my_pv/elwa_e/config.py b/packages/modules/consumers/my_pv/elwa_e/config.py new file mode 100644 index 0000000000..c41b65881f --- /dev/null +++ b/packages/modules/consumers/my_pv/elwa_e/config.py @@ -0,0 +1,31 @@ +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 ElwaConfiguration: + 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 Elwa(ConsumerSetup[ElwaConfiguration]): + def __init__(self, + name: str = "my-PV Elwa-E Heizstab", + type: str = "elwa_e", + id: int = 0, + configuration: ElwaConfiguration = 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 ElwaConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/my_pv/elwa_e/consumer.py b/packages/modules/consumers/my_pv/elwa_e/consumer.py new file mode 100644 index 0000000000..779fb41c6b --- /dev/null +++ b/packages/modules/consumers/my_pv/elwa_e/consumer.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +from enum import IntEnum +import logging +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.my_pv.elwa_e.config import Elwa + +log = logging.getLogger(__name__) + + +class Register(IntEnum): + POWER = 1000 + TEMP0 = 1001 + STATUS = 1003 + + +REG_MAPPING = ( + (Register.POWER, [ModbusDataType.INT_16]), + (Register.TEMP0, [ModbusDataType.INT_16]), + (Register.STATUS, ModbusDataType.INT_16), +) + +STATUS = { + 2: "Heat", + 3: "Standby", + 4: "Boost heat", + 5: "Heat finished", + 9: "Setup", + 201: "Error Overtemp Fuse blown", + 202: "Error Overtemp measured", + 203: "Error Overtemp Electronics", + 204: "Error Hardware Fault", + 205: "Error Temp Sensor" +} + + +def create_consumer(config: Elwa): + client = None + fuse = 1 + power = 0 + sim_counter = None + status = None + + def initializer(): + nonlocal client, fuse, sim_counter + client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) + fuse = client.read_input_registers(1014, ModbusDataType.INT_16, unit=config.configuration.modbus_id) + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + + def error_handler() -> None: + initializer() + + def update() -> ConsumerState: + nonlocal status + resp = client.read_holding_registers_bulk( + Register.POWER, 4, mapping=REG_MAPPING, unit=config.configuration.modbus_id) + power = resp[Register.POWER] + status = resp[Register.STATUS] + if status > 200: + raise Exception(f"Elwa-E meldet einen Fehler-Status: {STATUS.get(status, 'Unknown status')}") + imported, exported = sim_counter.sim_count(power) + return ConsumerState( + power=power, + imported=imported, + exported=exported, + temperatures=[resp[Register.TEMP0]/10] + ) + + def set_limit(power_limit: float) -> None: + if status == 4: + log.debug("Elwa-E im Boost-Heat Modus, keine Leistungsvorgabe möglich") + return + if power_limit < power: + power_limit = power + (power - power_limit) * fuse + power_limit = min(power_limit, 4000) + power_limit = min(power_limit, 0) + + client.write_registers(1000, power_limit, unit=config.configuration.modbus_id) + return ConfigurableConsumer(consumer_config=config, + initializer=initializer, + error_handler=error_handler, + update=update, + set_power_limit=set_limit,) + + +device_descriptor = DeviceDescriptor(configuration_factory=Elwa) diff --git a/packages/modules/consumers/my_pv/vendor.py b/packages/modules/consumers/my_pv/vendor.py new file mode 100644 index 0000000000..8cae7ed9b8 --- /dev/null +++ b/packages/modules/consumers/my_pv/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 = "my-PV" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) diff --git a/packages/modules/consumers/mystrom/__init__.py b/packages/modules/consumers/mystrom/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/mystrom/mystrom/__init__.py b/packages/modules/consumers/mystrom/mystrom/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/mystrom/mystrom/config.py b/packages/modules/consumers/mystrom/mystrom/config.py new file mode 100644 index 0000000000..3532dff044 --- /dev/null +++ b/packages/modules/consumers/mystrom/mystrom/config.py @@ -0,0 +1,26 @@ +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 MyStromConfiguration: + def __init__(self, ip_address: Optional[str] = None): + self.ip_address = ip_address + + +@auto_str +class MyStrom(ConsumerSetup[MyStromConfiguration]): + def __init__(self, + name: str = "MyStrom", + type: str = "mystrom", + id: int = 0, + configuration: MyStromConfiguration = None, + usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_ONOFF, + ConsumerUsage.METER_ONLY), + **kwargs) -> None: + super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( + ).type, configuration=configuration or MyStromConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/mystrom/mystrom/consumer.py b/packages/modules/consumers/mystrom/mystrom/consumer.py new file mode 100644 index 0000000000..82b07b13ad --- /dev/null +++ b/packages/modules/consumers/mystrom/mystrom/consumer.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +import logging +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 +from modules.common.simcount._simcounter import SimCounterConsumer +from modules.consumers.mystrom.mystrom.config import MyStrom + +log = logging.getLogger(__name__) + + +def create_consumer(config: MyStrom): + session = None + sim_counter = None + + def initializer(): + nonlocal session, sim_counter + session = req.get_http_session() + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + + def error_handler() -> None: + initializer() + + def update() -> ConsumerState: + resp = session.get(f"http://{config.configuration.ip_address}/report", timeout=3).json() + power = resp["power"] + relais = resp["relay"] + temp = float(resp["temperature"]) + imported, exported = sim_counter.sim_count(power) + return ConsumerState( + power=power, + imported=imported, + exported=exported, + temperatures=[temp], + state=relais + ) + + def switch_on() -> None: + session.get(f"http://{config.configuration.ip_address}/relay?state=1", timeout=3) + + def switch_off() -> None: + session.get(f"http://{config.configuration.ip_address}/relay?state=0", timeout=3) + + return ConfigurableConsumer(consumer_config=config, + initializer=initializer, + error_handler=error_handler, + update=update, + switch_on=switch_on, + switch_off=switch_off, + ) + + +device_descriptor = DeviceDescriptor(configuration_factory=MyStrom) diff --git a/packages/modules/consumers/mystrom/vendor.py b/packages/modules/consumers/mystrom/vendor.py new file mode 100644 index 0000000000..9e36f5507c --- /dev/null +++ b/packages/modules/consumers/mystrom/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 = "MyStrom" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) diff --git a/packages/modules/consumers/nibe/__init__.py b/packages/modules/consumers/nibe/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/nibe/nibe_s_series/__init__.py b/packages/modules/consumers/nibe/nibe_s_series/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/nibe/nibe_s_series/config.py b/packages/modules/consumers/nibe/nibe_s_series/config.py new file mode 100644 index 0000000000..5168d0de78 --- /dev/null +++ b/packages/modules/consumers/nibe/nibe_s_series/config.py @@ -0,0 +1,28 @@ +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 NibeConfiguration: + def __init__(self, + ip_address: Optional[str] = None, + modbus_id: Optional[int] = 1): + self.ip_address = ip_address + self.modbus_id = modbus_id + + +@auto_str +class Nibe(ConsumerSetup[NibeConfiguration]): + def __init__(self, + name: str = "Nibe S-Series Wärmepumpe", + type: str = "nibe_s_series", + id: int = 0, + configuration: NibeConfiguration = None, + usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.METER_ONLY,), + **kwargs) -> None: + super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( + ).type, configuration=configuration or NibeConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/nibe/nibe_s_series/consumer.py b/packages/modules/consumers/nibe/nibe_s_series/consumer.py new file mode 100644 index 0000000000..bf2142caf5 --- /dev/null +++ b/packages/modules/consumers/nibe/nibe_s_series/consumer.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +import logging +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.nibe.nibe_s_series.config import Nibe + +log = logging.getLogger(__name__) + + +def create_consumer(config: Nibe): + client = None + sim_counter = None + + def initializer(): + nonlocal client, sim_counter + client = ModbusTcpClient_(config.configuration.ip_address, 502) + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + + def error_handler() -> None: + initializer() + + def update() -> ConsumerState: + power = client.read_input_registers(2166, ModbusDataType.UINT_32, + unit=config.configuration.modbus_id) # / 10 lt Doku? + imported, exported = sim_counter.sim_count(power) + return ConsumerState( + power=power, + imported=imported, + exported=exported + ) + + return ConfigurableConsumer(consumer_config=config, + initializer=initializer, + error_handler=error_handler, + update=update) + + +device_descriptor = DeviceDescriptor(configuration_factory=Nibe) diff --git a/packages/modules/consumers/nibe/vendor.py b/packages/modules/consumers/nibe/vendor.py new file mode 100644 index 0000000000..48ccad8553 --- /dev/null +++ b/packages/modules/consumers/nibe/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 = "Nibe" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) 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) diff --git a/packages/modules/consumers/ratiotherm/__init__.py b/packages/modules/consumers/ratiotherm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/ratiotherm/ratiotherm/__init__.py b/packages/modules/consumers/ratiotherm/ratiotherm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/ratiotherm/ratiotherm/config.py b/packages/modules/consumers/ratiotherm/ratiotherm/config.py new file mode 100644 index 0000000000..4c325d0500 --- /dev/null +++ b/packages/modules/consumers/ratiotherm/ratiotherm/config.py @@ -0,0 +1,30 @@ +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 RatiothermConfiguration: + 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 Ratiotherm(ConsumerSetup[RatiothermConfiguration]): + def __init__(self, + name: str = "Ratiotherm Wärmepumpe", + type: str = "ratiotherm", + id: int = 0, + configuration: RatiothermConfiguration = None, + usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_TUNABLE,), + **kwargs) -> None: + super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( + ).type, configuration=configuration or RatiothermConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/ratiotherm/ratiotherm/consumer.py b/packages/modules/consumers/ratiotherm/ratiotherm/consumer.py new file mode 100644 index 0000000000..cd8cd4f06c --- /dev/null +++ b/packages/modules/consumers/ratiotherm/ratiotherm/consumer.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +import logging +from modules.common.abstract_device import DeviceDescriptor +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.ratiotherm.ratiotherm.config import Ratiotherm + +log = logging.getLogger(__name__) + + +def create_consumer(config: Ratiotherm): + 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 set_limit(power_limit: float) -> None: + # Absturz bei negativen Zahlen + client.write_register(100, max(power_limit, 0), ModbusDataType.INT_16, unit=config.configuration.modbus_id) + + return ConfigurableConsumer(consumer_config=config, + initializer=initializer, + error_handler=error_handler, + set_power_limit=set_limit,) + + +device_descriptor = DeviceDescriptor(configuration_factory=Ratiotherm) diff --git a/packages/modules/consumers/ratiotherm/vendor.py b/packages/modules/consumers/ratiotherm/vendor.py new file mode 100644 index 0000000000..7e6f5af780 --- /dev/null +++ b/packages/modules/consumers/ratiotherm/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 = "Ratiotherm" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) diff --git a/packages/modules/consumers/shelly/__init__.py b/packages/modules/consumers/shelly/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/shelly/shelly_em/__init__.py b/packages/modules/consumers/shelly/shelly_em/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/shelly/shelly_em/config.py b/packages/modules/consumers/shelly/shelly_em/config.py new file mode 100644 index 0000000000..2d3169acfa --- /dev/null +++ b/packages/modules/consumers/shelly/shelly_em/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 ShellyConfiguration: + def __init__(self, + ip_address: Optional[str] = None, + factor: Optional[int] = -1, + phase: Optional[int] = 1, + username: Optional[str] = None, + password: Optional[str] = None) -> None: + self.ip_address = ip_address + self.factor = factor + self.phase = phase + self.username = username + self.password = password + + +@auto_str +class ShellyEM(ConsumerSetup[ShellyConfiguration]): + def __init__(self, + name: str = "Shelly EM (Messen)", + type: str = "shelly_em", + id: int = 0, + configuration: ShellyConfiguration = None, + usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.METER_ONLY,), + **kwargs) -> None: + super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( + ).type, configuration=configuration or ShellyConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/shelly/shelly_em/consumer.py b/packages/modules/consumers/shelly/shelly_em/consumer.py new file mode 100644 index 0000000000..f7e83e4cc9 --- /dev/null +++ b/packages/modules/consumers/shelly/shelly_em/consumer.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +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.simcount._simcounter import SimCounterConsumer +from modules.consumers.shelly.shelly_em.config import ShellyEM +from modules.devices.shelly.shelly.status_handler import get_generation, request_status, parse_data + + +def create_consumer(config: ShellyEM): + sim_counter = None + generation = 1 + + def initializer(): + nonlocal sim_counter, generation + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + generation, _ = get_generation(config.configuration.ip_address) + + def error_handler() -> None: + initializer() + + def update() -> ConsumerState: + status = request_status(config.configuration.ip_address, generation) + powers, voltages, currents, _, power, _ = parse_data( + config.configuration.phase, config.configuration.factor, status) + imported, exported = sim_counter.sim_count(power) + consumer_state = ConsumerState( + power=power, + imported=imported, + exported=exported, + powers=powers, + ) + if voltages is not None: + consumer_state.voltages = voltages + if currents is not None: + consumer_state.currents = currents + return consumer_state + + return ConfigurableConsumer(consumer_config=config, + initializer=initializer, + error_handler=error_handler, + update=update,) + + +device_descriptor = DeviceDescriptor(configuration_factory=ShellyEM) diff --git a/packages/modules/consumers/shelly/shelly_pm/__init__.py b/packages/modules/consumers/shelly/shelly_pm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/shelly/shelly_pm/config.py b/packages/modules/consumers/shelly/shelly_pm/config.py new file mode 100644 index 0000000000..d0676e9a84 --- /dev/null +++ b/packages/modules/consumers/shelly/shelly_pm/config.py @@ -0,0 +1,37 @@ +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 ShellyConfiguration: + def __init__(self, + ip_address: Optional[str] = None, + factor: Optional[int] = -1, + phase: Optional[int] = 1, + channel: int = 0, + username: Optional[str] = None, + password: Optional[str] = None) -> None: + self.ip_address = ip_address + self.factor = factor + self.phase = phase + self.channel = channel + self.username = username + self.password = password + + +@auto_str +class ShellyPM(ConsumerSetup[ShellyConfiguration]): + def __init__(self, + name: str = "Shelly PM (Messen & Schalten)", + type: str = "shelly_pm", + id: int = 0, + configuration: ShellyConfiguration = None, + usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.METER_ONLY, + ConsumerUsage.SUSPENDABLE_ONOFF), + **kwargs) -> None: + super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( + ).type, configuration=configuration or ShellyConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/shelly/shelly_pm/consumer.py b/packages/modules/consumers/shelly/shelly_pm/consumer.py new file mode 100644 index 0000000000..db4ac15646 --- /dev/null +++ b/packages/modules/consumers/shelly/shelly_pm/consumer.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +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 +from modules.common.simcount._simcounter import SimCounterConsumer +from modules.consumers.shelly.shelly_pm.config import ShellyPM +from modules.devices.shelly.shelly.status_handler import get_generation, request_status, parse_data + + +def create_consumer(config: ShellyPM): + sim_counter = model = None + generation = 1 + + def initializer(): + nonlocal sim_counter, generation, model + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + generation, model = get_generation(config.configuration.ip_address) + + def error_handler() -> None: + initializer() + + def switch_on() -> None: + if generation == 1: + url = f"http://{config.configuration.ip_address}/relay/{config.configuration.channel}?turn=on" + else: + # shelly pro 3em mit add on hat fix id 100 als switch Kanal, das Device muss auf jeden fall mit separater + # Leistunsmessung erfasst werden, da die Leistung auf drei verschiedenenen Kanälen angeliefert werden kann + if "SPEM-003CE" in model: + chan = 100 + else: + chan = config.configuration.channel + # gen 2 will das als on cmd /rpc/Switch.Set?id=100&on=true + url = f"http://{config.configuration.ip_address}/rpc/Switch.Set?id={chan}&on=true" + if config.configuration.username and config.configuration.password: + auth = (config.configuration.username, config.configuration.password) + else: + auth = None + req.get_http_session().get(url, auth=auth, timeout=3) + + def switch_off() -> None: + if generation == 1: + url = f"http://{config.configuration.ip_address}/relay/{config.configuration.channel}?turn=off" + else: + # shelly pro 3em mit add on hat fix id 100 als switch Kanal, das Device muss auf jeden fall mit separater + # Leistunsmessung erfasst werden, da die Leistung auf drei verschiedenenen Kanälen angeliefert werden kann + if "SPEM-003CE" in model: + chan = 100 + else: + chan = config.configuration.channel + # gen 2 will das als on cmd /rpc/Switch.Set?id=100&on=true + url = f"http://{config.configuration.ip_address}/rpc/Switch.Set?id={chan}&on=false" + if config.configuration.username and config.configuration.password: + auth = (config.configuration.username, config.configuration.password) + else: + auth = None + req.get_http_session().get(url, auth=auth, timeout=3) + + def update() -> ConsumerState: + status = request_status(config.configuration.ip_address, generation) + powers, voltages, currents, _, power, _ = parse_data( + config.configuration.phase, config.configuration.factor, status) + imported, exported = sim_counter.sim_count(power) + return ConsumerState( + power=power, + imported=imported, + exported=exported, + voltages=voltages, + currents=currents, + powers=powers, + ) + + return ConfigurableConsumer(consumer_config=config, + initializer=initializer, + error_handler=error_handler, + switch_on=switch_on, + switch_off=switch_off, + update=update,) + + +device_descriptor = DeviceDescriptor(configuration_factory=ShellyPM) diff --git a/packages/modules/consumers/shelly/vendor.py b/packages/modules/consumers/shelly/vendor.py new file mode 100644 index 0000000000..f4c9e8c5b2 --- /dev/null +++ b/packages/modules/consumers/shelly/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 = "Shelly" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) diff --git a/packages/modules/consumers/solvis/__init__.py b/packages/modules/consumers/solvis/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/solvis/solvis/__init__.py b/packages/modules/consumers/solvis/solvis/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/solvis/solvis/config.py b/packages/modules/consumers/solvis/solvis/config.py new file mode 100644 index 0000000000..564832c8da --- /dev/null +++ b/packages/modules/consumers/solvis/solvis/config.py @@ -0,0 +1,27 @@ +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 SolvisConfiguration: + 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 SolvisHeatPump(ConsumerSetup[SolvisConfiguration]): + def __init__(self, + name: str = "Solvis Wärmepumpe", + type: str = "solvis", + id: int = 0, + configuration: SolvisConfiguration = None, + usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.METER_ONLY,), + **kwargs) -> None: + super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( + ).type, configuration=configuration or SolvisConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/solvis/solvis/consumer.py b/packages/modules/consumers/solvis/solvis/consumer.py new file mode 100644 index 0000000000..3830a9d2f4 --- /dev/null +++ b/packages/modules/consumers/solvis/solvis/consumer.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +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.solvis.solvis.config import SolvisHeatPump + + +def create_consumer(config: SolvisHeatPump): + 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 update() -> ConsumerState: + power = client.read_input_registers(33545, ModbusDataType.INT_16, unit=config.configuration.modbus_id) * 100 + imported, exported = sim_counter.sim_count(power) + return ConsumerState( + power=power, + imported=imported, + exported=exported + ) + + return ConfigurableConsumer(consumer_config=config, + initializer=initializer, + error_handler=error_handler, + update=update) + + +device_descriptor = DeviceDescriptor(configuration_factory=SolvisHeatPump) diff --git a/packages/modules/consumers/solvis/vendor.py b/packages/modules/consumers/solvis/vendor.py new file mode 100644 index 0000000000..2adee7c9c1 --- /dev/null +++ b/packages/modules/consumers/solvis/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 = "Solvis" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) diff --git a/packages/modules/consumers/stiebel/__init__.py b/packages/modules/consumers/stiebel/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/stiebel/stiebel/__init__.py b/packages/modules/consumers/stiebel/stiebel/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/stiebel/stiebel/config.py b/packages/modules/consumers/stiebel/stiebel/config.py new file mode 100644 index 0000000000..aa940eef11 --- /dev/null +++ b/packages/modules/consumers/stiebel/stiebel/config.py @@ -0,0 +1,30 @@ +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 StiebelConfiguration: + 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 Stiebel(ConsumerSetup[StiebelConfiguration]): + def __init__(self, + name: str = "Stiebel", + type: str = "stiebel", + id: int = 0, + configuration: StiebelConfiguration = None, + usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_ONOFF,), + **kwargs) -> None: + super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( + ).type, configuration=configuration or StiebelConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/stiebel/stiebel/consumer.py b/packages/modules/consumers/stiebel/stiebel/consumer.py new file mode 100644 index 0000000000..c7d39a3a1e --- /dev/null +++ b/packages/modules/consumers/stiebel/stiebel/consumer.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +import logging +from modules.common.abstract_device import DeviceDescriptor +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.stiebel.stiebel.config import Stiebel + +log = logging.getLogger(__name__) + + +def create_consumer(config: Stiebel): + 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 switch_on() -> None: + client.write_register(4001, 1, ModbusDataType.INT_16, unit=config.configuration.modbus_id) + + def switch_off() -> None: + client.write_register(4001, 0, ModbusDataType.INT_16, unit=config.configuration.modbus_id) + + return ConfigurableConsumer(consumer_config=config, + initializer=initializer, + error_handler=error_handler, + switch_on=switch_on, + switch_off=switch_off, + ) + + +device_descriptor = DeviceDescriptor(configuration_factory=Stiebel) diff --git a/packages/modules/consumers/stiebel/vendor.py b/packages/modules/consumers/stiebel/vendor.py new file mode 100644 index 0000000000..4e20a18158 --- /dev/null +++ b/packages/modules/consumers/stiebel/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 = "Stiebel" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) diff --git a/packages/modules/consumers/viessmann/__init__.py b/packages/modules/consumers/viessmann/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/viessmann/vendor.py b/packages/modules/consumers/viessmann/vendor.py new file mode 100644 index 0000000000..31a0aa7195 --- /dev/null +++ b/packages/modules/consumers/viessmann/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 = "Viessmann" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) diff --git a/packages/modules/consumers/viessmann/viessmann_heat_pump/__init__.py b/packages/modules/consumers/viessmann/viessmann_heat_pump/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/viessmann/viessmann_heat_pump/config.py b/packages/modules/consumers/viessmann/viessmann_heat_pump/config.py new file mode 100644 index 0000000000..6274b60698 --- /dev/null +++ b/packages/modules/consumers/viessmann/viessmann_heat_pump/config.py @@ -0,0 +1,27 @@ +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 ViessmannConfiguration: + 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 ViessmannHeatPump(ConsumerSetup[ViessmannConfiguration]): + def __init__(self, + name: str = "Viessmann Wärmepumpe", + type: str = "viessmann_heat_pump", + id: int = 0, + configuration: ViessmannConfiguration = None, + usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_ONOFF,), + **kwargs) -> None: + super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( + ).type, configuration=configuration or ViessmannConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/viessmann/viessmann_heat_pump/consumer.py b/packages/modules/consumers/viessmann/viessmann_heat_pump/consumer.py new file mode 100644 index 0000000000..cac0876d05 --- /dev/null +++ b/packages/modules/consumers/viessmann/viessmann_heat_pump/consumer.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +from modules.common.abstract_device import DeviceDescriptor +from modules.common.configurable_consumer import ConfigurableConsumer +from modules.common.modbus import ModbusTcpClient_ +from modules.consumers.viessmann.viessmann_heat_pump.config import ViessmannHeatPump + + +def create_consumer(config: ViessmannHeatPump): + client = None + + def initializer(): + nonlocal client + client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) + + def error_handler() -> None: + initializer() + + def switch_on() -> None: + client.write_single_coil(16, True, unit=config.configuration.modbus_id) + + def switch_off() -> None: + client.write_single_coil(16, False, unit=config.configuration.modbus_id) + + return ConfigurableConsumer(consumer_config=config, + initializer=initializer, + error_handler=error_handler, + switch_on=switch_on, + switch_off=switch_off,) + + +device_descriptor = DeviceDescriptor(configuration_factory=ViessmannHeatPump) From 7f24d8237627b80a8fc2897ab0441c196297497f Mon Sep 17 00:00:00 2001 From: LKuemmel <76958050+LKuemmel@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:06:43 +0200 Subject: [PATCH 2/4] Vampair (#3815) * Vampair * typo * review * review * review --- .../sample_consumer/consumer.py | 3 +- .../modules/consumers/generic/json/config.py | 4 +- .../consumers/generic/json/consumer.py | 6 +-- .../consumers/my_pv/elwa_e/consumer.py | 4 +- .../modules/consumers/solarfocus/__init__.py | 0 .../consumers/solarfocus/vampair/__init__.py | 0 .../consumers/solarfocus/vampair/config.py | 29 +++++++++++ .../consumers/solarfocus/vampair/consumer.py | 50 +++++++++++++++++++ .../modules/consumers/solarfocus/vendor.py | 14 ++++++ 9 files changed, 101 insertions(+), 9 deletions(-) create mode 100644 packages/modules/consumers/solarfocus/__init__.py create mode 100644 packages/modules/consumers/solarfocus/vampair/__init__.py create mode 100644 packages/modules/consumers/solarfocus/vampair/config.py create mode 100644 packages/modules/consumers/solarfocus/vampair/consumer.py create mode 100644 packages/modules/consumers/solarfocus/vendor.py diff --git a/docs/samples/sample_consumer/sample_consumer/consumer.py b/docs/samples/sample_consumer/sample_consumer/consumer.py index af25c43365..fb94155b6a 100644 --- a/docs/samples/sample_consumer/sample_consumer/consumer.py +++ b/docs/samples/sample_consumer/sample_consumer/consumer.py @@ -4,10 +4,11 @@ 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.askoma.askoheat.config import SampleConsumer +from modules.consumers.sample.sample.config import SampleConsumer log = logging.getLogger(__name__) diff --git a/packages/modules/consumers/generic/json/config.py b/packages/modules/consumers/generic/json/config.py index fda50af220..8d92c20b4c 100644 --- a/packages/modules/consumers/generic/json/config.py +++ b/packages/modules/consumers/generic/json/config.py @@ -21,9 +21,7 @@ def __init__(self, jq_switch_off: str = None, jq_set_power_limit: str = None): self.url = url - self.jq_current_l1 = jq_current_l1 - self.jq_current_l2 = jq_current_l2 - self.jq_current_l3 = jq_current_l3 + self.jq_currents = (jq_current_l1, jq_current_l2, jq_current_l3) self.jq_power = jq_power self.jq_temperatures = jq_temperatures self.jq_imported = jq_imported diff --git a/packages/modules/consumers/generic/json/consumer.py b/packages/modules/consumers/generic/json/consumer.py index 07f0e070d3..71b737bac2 100644 --- a/packages/modules/consumers/generic/json/consumer.py +++ b/packages/modules/consumers/generic/json/consumer.py @@ -78,15 +78,15 @@ def update() -> None: def switch_on(): # Authorization? - jq_switch_on(session, params={"state": True}) + jq_switch_on({"state": True}) def switch_off(): # Authorization? - jq_switch_off(session, params={"state": False}) + jq_switch_off({"state": False}) def set_power_limit(power_limit: int): # Authorization? - jq_set_power_limit(session, params={"power_limit": power_limit}) + jq_set_power_limit({"power_limit": power_limit}) return ConfigurableConsumer(consumer_config=config, initializer=initializer, diff --git a/packages/modules/consumers/my_pv/elwa_e/consumer.py b/packages/modules/consumers/my_pv/elwa_e/consumer.py index 779fb41c6b..2fc0312bfb 100644 --- a/packages/modules/consumers/my_pv/elwa_e/consumer.py +++ b/packages/modules/consumers/my_pv/elwa_e/consumer.py @@ -55,7 +55,7 @@ def error_handler() -> None: initializer() def update() -> ConsumerState: - nonlocal status + nonlocal power, status resp = client.read_holding_registers_bulk( Register.POWER, 4, mapping=REG_MAPPING, unit=config.configuration.modbus_id) power = resp[Register.POWER] @@ -77,7 +77,7 @@ def set_limit(power_limit: float) -> None: if power_limit < power: power_limit = power + (power - power_limit) * fuse power_limit = min(power_limit, 4000) - power_limit = min(power_limit, 0) + power_limit = max(power_limit, 0) client.write_registers(1000, power_limit, unit=config.configuration.modbus_id) return ConfigurableConsumer(consumer_config=config, diff --git a/packages/modules/consumers/solarfocus/__init__.py b/packages/modules/consumers/solarfocus/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/solarfocus/vampair/__init__.py b/packages/modules/consumers/solarfocus/vampair/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/solarfocus/vampair/config.py b/packages/modules/consumers/solarfocus/vampair/config.py new file mode 100644 index 0000000000..0de42b9fba --- /dev/null +++ b/packages/modules/consumers/solarfocus/vampair/config.py @@ -0,0 +1,29 @@ +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 VampairConfiguration: + 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 Vampair(ConsumerSetup[VampairConfiguration]): + def __init__(self, + name: str = "Vampair Wärmepumpe", + type: str = "vampair", + id: int = 0, + configuration: VampairConfiguration = None, + usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SELF_CONTROLLED, + ConsumerUsage.SUSPENDABLE_TUNABLE, + ConsumerUsage.METER_ONLY), + **kwargs) -> None: + super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( + ).type, configuration=configuration or VampairConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/solarfocus/vampair/consumer.py b/packages/modules/consumers/solarfocus/vampair/consumer.py new file mode 100644 index 0000000000..20b0440799 --- /dev/null +++ b/packages/modules/consumers/solarfocus/vampair/consumer.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +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.solarfocus.vampair.config import Vampair + + +def create_consumer(config: Vampair): + 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 update() -> ConsumerState: + power = client.read_holding_registers(2322, ModbusDataType.INT_16, unit=config.configuration.modbus_id) + imported, exported = sim_counter.sim_count(power) + return ConsumerState( + power=power, + imported=imported, + exported=exported + ) + + def set_power_limit(power_limit: float) -> None: + # Elektrische Sollleistung HEMS (PV) + client.write_register(33415, power_limit, unit=config.configuration.modbus_id) + client.write_register(33409, power_limit * -1, unit=config.configuration.modbus_id) + + def send_values(values: CurrentValues) -> None: + client.write_register(33408, values.pv_power * -1, unit=config.configuration.modbus_id) + client.write_register(33409, values.evu_power * -1, unit=config.configuration.modbus_id) + + return ConfigurableConsumer(consumer_config=config, + initializer=initializer, + error_handler=error_handler, + update=update, + set_power_limit=set_power_limit, + send_values=send_values) + + +device_descriptor = DeviceDescriptor(configuration_factory=Vampair) diff --git a/packages/modules/consumers/solarfocus/vendor.py b/packages/modules/consumers/solarfocus/vendor.py new file mode 100644 index 0000000000..fa7842045c --- /dev/null +++ b/packages/modules/consumers/solarfocus/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 = "Solarfocus" + self.group = VendorGroup.VENDORS.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) From 31a46f413a45678727e61c76e0fbfc9499574f4c Mon Sep 17 00:00:00 2001 From: LKuemmel <76958050+LKuemmel@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:04:20 +0200 Subject: [PATCH 3/4] add dataclass object for set_power_limit, DAC (#3855) * draft dac * add dataclass object for set_power_limit * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../sample_consumer/consumer.py | 6 +-- .../consumers/askoma/askoheat/consumer.py | 6 +-- .../modules/consumers/generic/dac/consumer.py | 43 ++++++++----------- .../consumers/generic/http/consumer.py | 4 +- .../consumers/generic/json/consumer.py | 4 +- .../modules/consumers/idm/idm/consumer.py | 6 +-- .../consumers/lambda_/lambda_/consumer.py | 6 +-- .../consumers/my_pv/acthor/consumer.py | 6 +-- .../consumers/my_pv/elwa_e/consumer.py | 6 +-- .../modules/consumers/ovum/ovum/consumer.py | 6 +-- .../ratiotherm/ratiotherm/consumer.py | 10 ++--- .../consumers/solarfocus/vampair/consumer.py | 4 +- 12 files changed, 51 insertions(+), 56 deletions(-) diff --git a/docs/samples/sample_consumer/sample_consumer/consumer.py b/docs/samples/sample_consumer/sample_consumer/consumer.py index fb94155b6a..5d7939788d 100644 --- a/docs/samples/sample_consumer/sample_consumer/consumer.py +++ b/docs/samples/sample_consumer/sample_consumer/consumer.py @@ -5,7 +5,7 @@ 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.configurable_consumer import ConfigurableConsumer, SetLimitData from modules.common.modbus import ModbusDataType, ModbusTcpClient_ from modules.common.simcount._simcounter import SimCounterConsumer from modules.consumers.sample.sample.config import SampleConsumer @@ -55,7 +55,7 @@ def update() -> ConsumerState: ) # ConsumerUsage.SUSPENDABLE_TUNABLE - def set_limit(power_limit: float) -> None: + def set_power_limit(power_limit: float, data: SetLimitData) -> None: nonlocal client client.write_registers(1000, power_limit, unit=config.configuration.modbus_id) @@ -80,7 +80,7 @@ def send_values(values: CurrentValues) -> None: error_handler=error_handler, update=update, send_values=send_values, - set_power_limit=set_limit, + set_power_limit=set_power_limit, switch_on=switch_on, switch_off=switch_off) diff --git a/packages/modules/consumers/askoma/askoheat/consumer.py b/packages/modules/consumers/askoma/askoheat/consumer.py index 71d9728a2b..535c05bf30 100644 --- a/packages/modules/consumers/askoma/askoheat/consumer.py +++ b/packages/modules/consumers/askoma/askoheat/consumer.py @@ -3,7 +3,7 @@ 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.configurable_consumer import ConfigurableConsumer, SetLimitData from modules.common.modbus import ModbusDataType, ModbusTcpClient_ from modules.common.simcount._simcounter import SimCounterConsumer from modules.consumers.askoma.askoheat.config import Askoheat @@ -33,14 +33,14 @@ def update() -> ConsumerState: temperatures=[client.read_input_registers(638, ModbusDataType.INT_16, unit=config.configuration.modbus_id)] ) - def set_limit(power_limit: float) -> None: + def set_power_limit(power_limit: float, data: SetLimitData) -> None: client.write_register(201, power_limit, unit=config.configuration.modbus_id) return ConfigurableConsumer(consumer_config=config, initializer=initializer, error_handler=error_handler, update=update, - set_power_limit=set_limit,) + set_power_limit=set_power_limit,) device_descriptor = DeviceDescriptor(configuration_factory=Askoheat) diff --git a/packages/modules/consumers/generic/dac/consumer.py b/packages/modules/consumers/generic/dac/consumer.py index 654f2ab1c5..e25b462dba 100644 --- a/packages/modules/consumers/generic/dac/consumer.py +++ b/packages/modules/consumers/generic/dac/consumer.py @@ -2,8 +2,8 @@ import logging from modules.common.abstract_device import DeviceDescriptor from modules.common.component_type import ComponentType -from modules.common.configurable_consumer import ConfigurableConsumer -from modules.common.modbus import ModbusTcpClient_ +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.generic.dac.config import Dac from modules.consumers.generic.dac.model import Model @@ -12,7 +12,7 @@ def create_consumer(config: Dac): - client = None + client: ModbusTcpClient_ = None sim_counter = None def initializer(): @@ -23,31 +23,26 @@ def initializer(): def error_handler() -> None: initializer() - def set_limit(power_limit: float) -> None: - if config.configuration.model == Model.N4Dac02 or config.configuration.model == Model.M120T: - # pwoer_limit -= power - pass - - # power_limit = max(power_limit, 0) - # if config.configuration.model == Model.N4Dac02: - # client.write_register(1, power_limit * 1000 / max_power, ModbusDataType.INT_16, - # unit=config.configuration.modbus_id) - # elif config.configuration.model == Model.DA02: - # client.write_register(0x01f4, power_limit * 4000 / max_power, - # ModbusDataType.INT_16, unit=config.configuration.modbus_id) - # elif config.configuration.model == Model.M120T: - # # ausgabe nicht kleiner 0,9V sonst Leistungsregelung der WP aus - # power_limit = max(power_limit * 4095 / max_power, 370) - # client.write_register(0x01f4, power_limit, ModbusDataType.INT_16, unit=config.configuration.modbus_id) - # elif config.configuration.model == Model.AA02B: - # power_limit = max((power_limit * (4095-820) / max_power)+820, 820) - # # ausgabe nicht kleiner 4ma sonst Leistungsregelung der WP aus - # client.write_register(0x01f4, power_limit, ModbusDataType.INT_16, unit=config.configuration.modbus_id) + def set_power_limit(power_limit: float, data: SetLimitData) -> None: + power_limit = max(power_limit, 0) + modbus_id = config.configuration.modbus_id + if config.configuration.model == Model.N4Dac02: + client.write_register(1, power_limit * 1000 / data.max_power, ModbusDataType.INT_16, unit=modbus_id) + elif config.configuration.model == Model.DA02: + client.write_register(0x01f4, power_limit * 4000 / data.max_power, ModbusDataType.INT_16, unit=modbus_id) + elif config.configuration.model == Model.M120T: + # ausgabe nicht kleiner 0,9V sonst Leistungsregelung der WP aus + power_limit = max(power_limit * 4095 / data.max_power, 370) + client.write_register(0x01f4, power_limit, ModbusDataType.INT_16, unit=modbus_id) + elif config.configuration.model == Model.AA02B: + power_limit = max((power_limit * (4095-820) / data.max_power)+820, 820) + # ausgabe nicht kleiner 4ma sonst Leistungsregelung der WP aus + client.write_register(0x01f4, power_limit, ModbusDataType.INT_16, unit=modbus_id) return ConfigurableConsumer(consumer_config=config, initializer=initializer, error_handler=error_handler, - set_power_limit=set_limit,) + set_power_limit=set_power_limit,) device_descriptor = DeviceDescriptor(configuration_factory=Dac) diff --git a/packages/modules/consumers/generic/http/consumer.py b/packages/modules/consumers/generic/http/consumer.py index 080e4983ad..020c7bf3bb 100644 --- a/packages/modules/consumers/generic/http/consumer.py +++ b/packages/modules/consumers/generic/http/consumer.py @@ -5,7 +5,7 @@ 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.configurable_consumer import ConfigurableConsumer, SetLimitData from modules.common.simcount._simcounter import SimCounterConsumer from modules.consumers.generic.http.config import Http from modules.devices.generic.http.api import (create_post_function, create_request_function, @@ -71,7 +71,7 @@ def switch_off(): # Authorization? post_switch_off(session, params={"state": False}) - def set_power_limit(power_limit: int): + def set_power_limit(power_limit: float, data: SetLimitData) -> None: # Authorization? post_set_power_limit(session, params={"power_limit": power_limit}) diff --git a/packages/modules/consumers/generic/json/consumer.py b/packages/modules/consumers/generic/json/consumer.py index 71b737bac2..34caecd075 100644 --- a/packages/modules/consumers/generic/json/consumer.py +++ b/packages/modules/consumers/generic/json/consumer.py @@ -7,7 +7,7 @@ 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.configurable_consumer import ConfigurableConsumer, SetLimitData from modules.common.simcount._simcounter import SimCounterConsumer from modules.consumers.generic.json.config import Json @@ -84,7 +84,7 @@ def switch_off(): # Authorization? jq_switch_off({"state": False}) - def set_power_limit(power_limit: int): + def set_power_limit(power_limit: float, data: SetLimitData) -> None: # Authorization? jq_set_power_limit({"power_limit": power_limit}) diff --git a/packages/modules/consumers/idm/idm/consumer.py b/packages/modules/consumers/idm/idm/consumer.py index d3a6b27d38..f32116ce64 100644 --- a/packages/modules/consumers/idm/idm/consumer.py +++ b/packages/modules/consumers/idm/idm/consumer.py @@ -6,7 +6,7 @@ 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.configurable_consumer import ConfigurableConsumer, SetLimitData from modules.common.modbus import ModbusDataType, ModbusTcpClient_ from modules.common.simcount._simcounter import SimCounterConsumer from modules.consumers.idm.idm.config import Idm @@ -71,7 +71,7 @@ def update() -> ConsumerState: exported=exported ) - def set_limit(power_limit: float) -> None: + def set_power_limit(power_limit: float, data: SetLimitData) -> 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 @@ -84,7 +84,7 @@ def set_limit(power_limit: float) -> None: error_handler=error_handler, update=update, send_values=send_values, - set_power_limit=set_limit) + set_power_limit=set_power_limit) device_descriptor = DeviceDescriptor(configuration_factory=Idm) diff --git a/packages/modules/consumers/lambda_/lambda_/consumer.py b/packages/modules/consumers/lambda_/lambda_/consumer.py index d31a3f9375..b65248d1f4 100644 --- a/packages/modules/consumers/lambda_/lambda_/consumer.py +++ b/packages/modules/consumers/lambda_/lambda_/consumer.py @@ -6,7 +6,7 @@ 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.configurable_consumer import ConfigurableConsumer, SetLimitData from modules.common.modbus import ModbusDataType, ModbusTcpClient_ from modules.common.simcount._simcounter import SimCounterConsumer from modules.consumers.lambda_.lambda_.config import Lambda @@ -45,7 +45,7 @@ def update() -> ConsumerState: exported=exported ) - def set_limit(power_limit: float) -> None: + def set_power_limit(power_limit: float, data: SetLimitData) -> 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, @@ -58,7 +58,7 @@ def set_limit(power_limit: float) -> None: error_handler=error_handler, update=update, send_values=send_values, - set_power_limit=set_limit,) + set_power_limit=set_power_limit,) device_descriptor = DeviceDescriptor(configuration_factory=Lambda) diff --git a/packages/modules/consumers/my_pv/acthor/consumer.py b/packages/modules/consumers/my_pv/acthor/consumer.py index f35b96d8dc..f35e63cab7 100644 --- a/packages/modules/consumers/my_pv/acthor/consumer.py +++ b/packages/modules/consumers/my_pv/acthor/consumer.py @@ -4,7 +4,7 @@ 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.configurable_consumer import ConfigurableConsumer, SetLimitData from modules.common.modbus import ModbusDataType, ModbusTcpClient_ from modules.common.simcount._simcounter import SimCounterConsumer from modules.consumers.my_pv.acthor.config import Acthor @@ -61,14 +61,14 @@ def update() -> ConsumerState: temperatures=[resp[Register.TEMP0]/10, resp[Register.TEMP1]/10, resp[Register.TEMP2]/10] ) - def set_limit(power_limit: float) -> None: + def set_power_limit(power_limit: float, data: SetLimitData) -> None: power_limit = power_limit * FACTORS.get(config.configuration.model, 9000)/config.configuration.max_power client.write_registers(1000, power_limit, unit=config.configuration.modbus_id) return ConfigurableConsumer(consumer_config=config, initializer=initializer, error_handler=error_handler, update=update, - set_power_limit=set_limit,) + set_power_limit=set_power_limit,) device_descriptor = DeviceDescriptor(configuration_factory=Acthor) diff --git a/packages/modules/consumers/my_pv/elwa_e/consumer.py b/packages/modules/consumers/my_pv/elwa_e/consumer.py index 2fc0312bfb..f5c7cfd598 100644 --- a/packages/modules/consumers/my_pv/elwa_e/consumer.py +++ b/packages/modules/consumers/my_pv/elwa_e/consumer.py @@ -4,7 +4,7 @@ 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.configurable_consumer import ConfigurableConsumer, SetLimitData from modules.common.modbus import ModbusDataType, ModbusTcpClient_ from modules.common.simcount._simcounter import SimCounterConsumer from modules.consumers.my_pv.elwa_e.config import Elwa @@ -70,7 +70,7 @@ def update() -> ConsumerState: temperatures=[resp[Register.TEMP0]/10] ) - def set_limit(power_limit: float) -> None: + def set_power_limit(power_limit: float, data: SetLimitData) -> None: if status == 4: log.debug("Elwa-E im Boost-Heat Modus, keine Leistungsvorgabe möglich") return @@ -84,7 +84,7 @@ def set_limit(power_limit: float) -> None: initializer=initializer, error_handler=error_handler, update=update, - set_power_limit=set_limit,) + set_power_limit=set_power_limit,) device_descriptor = DeviceDescriptor(configuration_factory=Elwa) diff --git a/packages/modules/consumers/ovum/ovum/consumer.py b/packages/modules/consumers/ovum/ovum/consumer.py index 48b6e9c5ce..3961f4ac53 100644 --- a/packages/modules/consumers/ovum/ovum/consumer.py +++ b/packages/modules/consumers/ovum/ovum/consumer.py @@ -6,7 +6,7 @@ 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.configurable_consumer import ConfigurableConsumer, SetLimitData from modules.common.modbus import ModbusDataType, ModbusTcpClient_ from modules.common.simcount._simcounter import SimCounterConsumer from modules.consumers.ovum.ovum.config import Ovum @@ -54,7 +54,7 @@ def update() -> ConsumerState: exported=exported ) - def set_limit(power_limit: Optional[float]) -> None: + def set_power_limit(power_limit: Optional[float], data: SetLimitData) -> 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 @@ -119,7 +119,7 @@ def switch_off() -> None: error_handler=error_handler, update=update, send_values=send_values, - set_power_limit=set_limit, + set_power_limit=set_power_limit, switch_on=switch_on, switch_off=switch_off) diff --git a/packages/modules/consumers/ratiotherm/ratiotherm/consumer.py b/packages/modules/consumers/ratiotherm/ratiotherm/consumer.py index cd8cd4f06c..0ab039c769 100644 --- a/packages/modules/consumers/ratiotherm/ratiotherm/consumer.py +++ b/packages/modules/consumers/ratiotherm/ratiotherm/consumer.py @@ -2,7 +2,7 @@ import logging from modules.common.abstract_device import DeviceDescriptor from modules.common.component_type import ComponentType -from modules.common.configurable_consumer import ConfigurableConsumer +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.ratiotherm.ratiotherm.config import Ratiotherm @@ -22,14 +22,14 @@ def initializer(): def error_handler() -> None: initializer() - def set_limit(power_limit: float) -> None: + def set_power_limit(power_limit: float, data: SetLimitData) -> None: # Absturz bei negativen Zahlen - client.write_register(100, max(power_limit, 0), ModbusDataType.INT_16, unit=config.configuration.modbus_id) - + raw_value = int(round(max(power_limit, 0))) + client.write_register(100, raw_value, ModbusDataType.INT_16, unit=config.configuration.modbus_id) return ConfigurableConsumer(consumer_config=config, initializer=initializer, error_handler=error_handler, - set_power_limit=set_limit,) + set_power_limit=set_power_limit,) device_descriptor = DeviceDescriptor(configuration_factory=Ratiotherm) diff --git a/packages/modules/consumers/solarfocus/vampair/consumer.py b/packages/modules/consumers/solarfocus/vampair/consumer.py index 20b0440799..812571a634 100644 --- a/packages/modules/consumers/solarfocus/vampair/consumer.py +++ b/packages/modules/consumers/solarfocus/vampair/consumer.py @@ -3,7 +3,7 @@ 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.configurable_consumer import ConfigurableConsumer, SetLimitData from modules.common.modbus import ModbusDataType, ModbusTcpClient_ from modules.common.simcount._simcounter import SimCounterConsumer from modules.consumers.solarfocus.vampair.config import Vampair @@ -30,7 +30,7 @@ def update() -> ConsumerState: exported=exported ) - def set_power_limit(power_limit: float) -> None: + def set_power_limit(power_limit: float, data: SetLimitData) -> None: # Elektrische Sollleistung HEMS (PV) client.write_register(33415, power_limit, unit=config.configuration.modbus_id) client.write_register(33409, power_limit * -1, unit=config.configuration.modbus_id) From fe169931e8b4faaf4ecf356f87cf1fbc41daa32b Mon Sep 17 00:00:00 2001 From: benderl Date: Fri, 28 Aug 2026 15:02:35 +0200 Subject: [PATCH 4/4] Fix consumer modules (#3865) * fix configuration type hints * fix consumer type hints and errors * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../consumers/askoma/askoheat/config.py | 6 ++--- .../consumers/askoma/askoheat/consumer.py | 5 ++-- packages/modules/consumers/avm/avm/config.py | 5 ++-- .../modules/consumers/avm/avm/consumer.py | 6 ++++- .../modules/consumers/generic/dac/config.py | 6 ++--- .../modules/consumers/generic/dac/consumer.py | 11 ++++---- .../modules/consumers/generic/http/config.py | 27 +++++++++---------- .../consumers/generic/http/consumer.py | 27 ++++++++++--------- .../modules/consumers/generic/json/config.py | 26 +++++++++--------- .../consumers/generic/json/consumer.py | 6 ++--- packages/modules/consumers/idm/idm/config.py | 6 ++--- .../modules/consumers/idm/idm/consumer.py | 5 ++-- .../consumers/lambda_/lambda_/config.py | 6 ++--- .../consumers/lambda_/lambda_/consumer.py | 5 ++-- .../modules/consumers/my_pv/acthor/config.py | 10 +++---- .../consumers/my_pv/acthor/consumer.py | 5 ++-- .../modules/consumers/my_pv/elwa_e/config.py | 6 ++--- .../consumers/my_pv/elwa_e/consumer.py | 23 ++++++++-------- .../consumers/mystrom/mystrom/config.py | 2 +- .../consumers/mystrom/mystrom/consumer.py | 5 ++-- .../consumers/nibe/nibe_s_series/config.py | 4 +-- .../consumers/nibe/nibe_s_series/consumer.py | 5 ++-- .../modules/consumers/ovum/ovum/config.py | 6 ++--- .../modules/consumers/ovum/ovum/consumer.py | 4 +-- .../consumers/ratiotherm/ratiotherm/config.py | 6 ++--- .../ratiotherm/ratiotherm/consumer.py | 6 +++-- .../consumers/shelly/shelly_em/config.py | 6 ++--- .../consumers/shelly/shelly_em/consumer.py | 5 ++-- .../consumers/shelly/shelly_pm/config.py | 6 ++--- .../consumers/shelly/shelly_pm/consumer.py | 6 +++-- .../consumers/solarfocus/vampair/config.py | 4 +-- .../consumers/solarfocus/vampair/consumer.py | 5 ++-- .../modules/consumers/solvis/solvis/config.py | 4 +-- .../consumers/solvis/solvis/consumer.py | 5 ++-- .../consumers/stiebel/stiebel/config.py | 6 ++--- .../consumers/stiebel/stiebel/consumer.py | 5 ++-- .../viessmann/viessmann_heat_pump/config.py | 4 +-- .../viessmann/viessmann_heat_pump/consumer.py | 3 ++- .../devices/shelly/shelly/status_handler.py | 2 +- 39 files changed, 155 insertions(+), 135 deletions(-) diff --git a/packages/modules/consumers/askoma/askoheat/config.py b/packages/modules/consumers/askoma/askoheat/config.py index 2559a81dc8..e2703fe704 100644 --- a/packages/modules/consumers/askoma/askoheat/config.py +++ b/packages/modules/consumers/askoma/askoheat/config.py @@ -10,8 +10,8 @@ class AskoheatConfiguration: def __init__(self, ip_address: Optional[str] = None, - port: Optional[int] = 502, - modbus_id: Optional[int] = 1): + port: int = 502, + modbus_id: int = 1): self.ip_address = ip_address self.port = port self.modbus_id = modbus_id @@ -23,7 +23,7 @@ def __init__(self, name: str = "Askoheat+", type: str = "askoheat", id: int = 0, - configuration: AskoheatConfiguration = None, + configuration: Optional[AskoheatConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_TUNABLE, ConsumerUsage.METER_ONLY), **kwargs) -> None: diff --git a/packages/modules/consumers/askoma/askoheat/consumer.py b/packages/modules/consumers/askoma/askoheat/consumer.py index 535c05bf30..8ec2495e4b 100644 --- a/packages/modules/consumers/askoma/askoheat/consumer.py +++ b/packages/modules/consumers/askoma/askoheat/consumer.py @@ -1,5 +1,6 @@ #!/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 @@ -12,8 +13,8 @@ def create_consumer(config: Askoheat): - client = None - sim_counter = None + client: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None def initializer(): nonlocal client, sim_counter diff --git a/packages/modules/consumers/avm/avm/config.py b/packages/modules/consumers/avm/avm/config.py index 40c6998564..b0adccac5b 100644 --- a/packages/modules/consumers/avm/avm/config.py +++ b/packages/modules/consumers/avm/avm/config.py @@ -1,5 +1,4 @@ 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 @@ -13,7 +12,7 @@ def __init__(self, username: Optional[str] = None, password: Optional[str] = None, session_id: Optional[str] = None, - session_mtime: Optional[str] = None, + session_mtime: Optional[float] = None, name: Optional[str] = None) -> None: self.ip_address = ip_address self.username = username @@ -29,7 +28,7 @@ def __init__(self, name: str = "AVM Fritz!Box", type: str = "avm", id: int = 0, - configuration: AvmConfiguration = None, + configuration: Optional[AvmConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_TUNABLE, ConsumerUsage.METER_ONLY), **kwargs) -> None: diff --git a/packages/modules/consumers/avm/avm/consumer.py b/packages/modules/consumers/avm/avm/consumer.py index 5b28d073c3..1b32aaa5bf 100644 --- a/packages/modules/consumers/avm/avm/consumer.py +++ b/packages/modules/consumers/avm/avm/consumer.py @@ -3,6 +3,7 @@ import logging import time import xml.etree.ElementTree as ET +from typing import Optional from dataclass_utils._dataclass_asdict import asdict from helpermodules.pub import Pub @@ -18,7 +19,7 @@ def create_consumer(config: Avm): - ain = None # Actuator Identification Number + ain: Optional[str] = None # Actuator Identification Number def update() -> ConsumerState: nonlocal ain @@ -61,6 +62,9 @@ def update() -> ConsumerState: state=state, voltages=voltages ) + raise RuntimeError( + f"Device with name '{config.configuration.name}' was not found or is not currently available" + ) def ensure_valid_session_id(): if check_valid_session_id() is False: diff --git a/packages/modules/consumers/generic/dac/config.py b/packages/modules/consumers/generic/dac/config.py index ac9e34cd40..2d2b847095 100644 --- a/packages/modules/consumers/generic/dac/config.py +++ b/packages/modules/consumers/generic/dac/config.py @@ -11,8 +11,8 @@ class DacConfiguration: def __init__(self, ip_address: Optional[str] = None, - port: Optional[int] = 502, - modbus_id: Optional[int] = 1, + port: int = 502, + modbus_id: int = 1, model: Model = Model.N4Dac02): self.ip_address = ip_address self.port = port @@ -26,7 +26,7 @@ def __init__(self, name: str = "Digital-Analog-Konverter (DAC) 0.01V bis 10.0V", type: str = "dac", id: int = 0, - configuration: DacConfiguration = None, + configuration: Optional[DacConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_TUNABLE,), **kwargs) -> None: super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( diff --git a/packages/modules/consumers/generic/dac/consumer.py b/packages/modules/consumers/generic/dac/consumer.py index e25b462dba..c0777cb960 100644 --- a/packages/modules/consumers/generic/dac/consumer.py +++ b/packages/modules/consumers/generic/dac/consumer.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import logging +from typing import Optional from modules.common.abstract_device import DeviceDescriptor from modules.common.component_type import ComponentType from modules.common.configurable_consumer import ConfigurableConsumer, SetLimitData @@ -12,8 +13,8 @@ def create_consumer(config: Dac): - client: ModbusTcpClient_ = None - sim_counter = None + client: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None def initializer(): nonlocal client, sim_counter @@ -31,18 +32,18 @@ def set_power_limit(power_limit: float, data: SetLimitData) -> None: elif config.configuration.model == Model.DA02: client.write_register(0x01f4, power_limit * 4000 / data.max_power, ModbusDataType.INT_16, unit=modbus_id) elif config.configuration.model == Model.M120T: - # ausgabe nicht kleiner 0,9V sonst Leistungsregelung der WP aus + # Ausgabe nicht kleiner 0,9V sonst Leistungsregelung der WP aus power_limit = max(power_limit * 4095 / data.max_power, 370) client.write_register(0x01f4, power_limit, ModbusDataType.INT_16, unit=modbus_id) elif config.configuration.model == Model.AA02B: power_limit = max((power_limit * (4095-820) / data.max_power)+820, 820) - # ausgabe nicht kleiner 4ma sonst Leistungsregelung der WP aus + # Ausgabe nicht kleiner 4ma sonst Leistungsregelung der WP aus client.write_register(0x01f4, power_limit, ModbusDataType.INT_16, unit=modbus_id) return ConfigurableConsumer(consumer_config=config, initializer=initializer, error_handler=error_handler, - set_power_limit=set_power_limit,) + set_power_limit=set_power_limit) device_descriptor = DeviceDescriptor(configuration_factory=Dac) diff --git a/packages/modules/consumers/generic/http/config.py b/packages/modules/consumers/generic/http/config.py index 6fe3eef212..83840f9448 100644 --- a/packages/modules/consumers/generic/http/config.py +++ b/packages/modules/consumers/generic/http/config.py @@ -1,5 +1,4 @@ -from typing import Tuple - +from typing import Tuple, Optional from control.consumer.consumer_data import ConsumerUsage from helpermodules.auto_str import auto_str from modules.common.consumer_setup import ConsumerSetup @@ -9,17 +8,17 @@ @auto_str class HttpConfiguration: def __init__(self, - url: str = None, - current_l1_path: str = None, - current_l2_path: str = None, - current_l3_path: str = None, - power_path: str = None, - temperatures_path: str = None, - imported_path: str = None, - exported_path: str = None, - switch_on_path: str = None, - switch_off_path: str = None, - set_power_limit_path: str = None): + url: Optional[str] = None, + current_l1_path: Optional[str] = None, + current_l2_path: Optional[str] = None, + current_l3_path: Optional[str] = None, + power_path: Optional[str] = None, + temperatures_path: Optional[str] = None, + imported_path: Optional[str] = None, + exported_path: Optional[str] = None, + switch_on_path: Optional[str] = None, + switch_off_path: Optional[str] = None, + set_power_limit_path: Optional[str] = None): self.url = url self.current_l1_path = current_l1_path self.current_l2_path = current_l2_path @@ -39,7 +38,7 @@ def __init__(self, name: str = "HTTP-Verbraucher", type: str = "http", id: int = 0, - configuration: HttpConfiguration = None, + configuration: Optional[HttpConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.METER_ONLY, ConsumerUsage.CONTINUOUS, ConsumerUsage.SUSPENDABLE_ONOFF, diff --git a/packages/modules/consumers/generic/http/consumer.py b/packages/modules/consumers/generic/http/consumer.py index 020c7bf3bb..ae7becc93f 100644 --- a/packages/modules/consumers/generic/http/consumer.py +++ b/packages/modules/consumers/generic/http/consumer.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import logging +from typing import Optional, Callable from modules.common import req from modules.common.abstract_device import DeviceDescriptor @@ -14,16 +15,16 @@ def create_consumer(config: Http): - session = None - sim_counter = None - get_power = None - get_imported = None - get_exported = None - get_currents = None - get_temperatures = None - post_set_power_limit = None - post_switch_on = None - post_switch_off = None + session: Optional[req.CustomSession] = None + sim_counter: Optional[SimCounterConsumer] = None + get_power: Optional[Callable] = None + get_imported: Optional[Callable] = None + get_exported: Optional[Callable] = None + get_currents: Optional[Callable] = None + get_temperatures: Optional[Callable] = None + post_set_power_limit: Optional[Callable] = None + post_switch_on: Optional[Callable] = None + post_switch_off: Optional[Callable] = None def initializer(): nonlocal session, sim_counter @@ -65,15 +66,15 @@ def update() -> None: def switch_on(): # Authorization? - post_switch_on(session, params={"state": True}) + post_switch_on(session, {"state": True}) def switch_off(): # Authorization? - post_switch_off(session, params={"state": False}) + post_switch_off(session, {"state": False}) def set_power_limit(power_limit: float, data: SetLimitData) -> None: # Authorization? - post_set_power_limit(session, params={"power_limit": power_limit}) + post_set_power_limit(session, {"power_limit": power_limit}) return ConfigurableConsumer(consumer_config=config, initializer=initializer, diff --git a/packages/modules/consumers/generic/json/config.py b/packages/modules/consumers/generic/json/config.py index 8d92c20b4c..708ca6e3cf 100644 --- a/packages/modules/consumers/generic/json/config.py +++ b/packages/modules/consumers/generic/json/config.py @@ -1,4 +1,4 @@ -from typing import Tuple +from typing import Tuple, Optional from control.consumer.consumer_data import ConsumerUsage from helpermodules.auto_str import auto_str @@ -9,17 +9,17 @@ @auto_str class JsonConfiguration: def __init__(self, - url: str = None, - jq_current_l1: str = None, - jq_current_l2: str = None, - jq_current_l3: str = None, - jq_power: str = None, - jq_temperatures: str = None, - jq_imported: str = None, - jq_exported: str = None, - jq_switch_on: str = None, - jq_switch_off: str = None, - jq_set_power_limit: str = None): + url: Optional[str] = None, + jq_current_l1: Optional[str] = None, + jq_current_l2: Optional[str] = None, + jq_current_l3: Optional[str] = None, + jq_power: Optional[str] = None, + jq_temperatures: Optional[str] = None, + jq_imported: Optional[str] = None, + jq_exported: Optional[str] = None, + jq_switch_on: Optional[str] = None, + jq_switch_off: Optional[str] = None, + jq_set_power_limit: Optional[str] = None): self.url = url self.jq_currents = (jq_current_l1, jq_current_l2, jq_current_l3) self.jq_power = jq_power @@ -37,7 +37,7 @@ def __init__(self, name: str = "JSON-Verbraucher", type: str = "json", id: int = 0, - configuration: JsonConfiguration = None, + configuration: Optional[JsonConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.METER_ONLY, ConsumerUsage.CONTINUOUS, ConsumerUsage.SUSPENDABLE_ONOFF, diff --git a/packages/modules/consumers/generic/json/consumer.py b/packages/modules/consumers/generic/json/consumer.py index 34caecd075..29676e13e3 100644 --- a/packages/modules/consumers/generic/json/consumer.py +++ b/packages/modules/consumers/generic/json/consumer.py @@ -15,8 +15,8 @@ def create_consumer(config: Json): - session = None - sim_counter = None + session: Optional[req.CustomSession] = None + sim_counter: Optional[SimCounterConsumer] = None jq_power = None jq_imported = None jq_exported = None @@ -57,7 +57,7 @@ def initializer(): jq_switch_on = create_post_function(config.configuration.jq_switch_on) jq_switch_off = create_post_function(config.configuration.jq_switch_off) - def update() -> None: + def update() -> ConsumerState: response = req.get_http_session().get(config.configuration.url, timeout=5).json() power = float(jq_power.input(response).first()) temperatures = float(jq_temperatures.input(response).first()) if jq_temperatures is not None else None diff --git a/packages/modules/consumers/idm/idm/config.py b/packages/modules/consumers/idm/idm/config.py index e417129637..5526ad04f8 100644 --- a/packages/modules/consumers/idm/idm/config.py +++ b/packages/modules/consumers/idm/idm/config.py @@ -10,8 +10,8 @@ class IdmConfiguration: def __init__(self, ip_address: Optional[str] = None, - port: Optional[int] = 502, - modbus_id: Optional[int] = 1, + port: int = 502, + modbus_id: int = 1, version: int = 1): self.ip_address = ip_address self.port = port @@ -25,7 +25,7 @@ def __init__(self, name: str = "Wärmepumpe der Firma IDM mit Navigatorregelung 1.7/2.0", type: str = "idm", id: int = 0, - configuration: IdmConfiguration = None, + configuration: Optional[IdmConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_TUNABLE, ConsumerUsage.SELF_CONTROLLED, ConsumerUsage.METER_ONLY), diff --git a/packages/modules/consumers/idm/idm/consumer.py b/packages/modules/consumers/idm/idm/consumer.py index f32116ce64..8f50b9b19c 100644 --- a/packages/modules/consumers/idm/idm/consumer.py +++ b/packages/modules/consumers/idm/idm/consumer.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 from pymodbus.constants import Endian import logging +from typing import Optional from modules.common.abstract_consumer import CurrentValues from modules.common.abstract_device import DeviceDescriptor @@ -15,8 +16,8 @@ def create_consumer(config: Idm): - client = None - sim_counter = None + client: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None def initializer(): nonlocal client, sim_counter diff --git a/packages/modules/consumers/lambda_/lambda_/config.py b/packages/modules/consumers/lambda_/lambda_/config.py index c02249395f..da06fddf4b 100644 --- a/packages/modules/consumers/lambda_/lambda_/config.py +++ b/packages/modules/consumers/lambda_/lambda_/config.py @@ -10,8 +10,8 @@ class LambdaConfiguration: def __init__(self, ip_address: Optional[str] = None, - port: Optional[int] = 502, - modbus_id: Optional[int] = 1): + port: int = 502, + modbus_id: int = 1): self.ip_address = ip_address self.port = port self.modbus_id = modbus_id @@ -23,7 +23,7 @@ def __init__(self, name: str = "Lambda Wärmepumpe", type: str = "lambda_", id: int = 0, - configuration: LambdaConfiguration = None, + configuration: Optional[LambdaConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_TUNABLE, ConsumerUsage.SELF_CONTROLLED, ConsumerUsage.METER_ONLY), diff --git a/packages/modules/consumers/lambda_/lambda_/consumer.py b/packages/modules/consumers/lambda_/lambda_/consumer.py index b65248d1f4..6c8cf7ba0e 100644 --- a/packages/modules/consumers/lambda_/lambda_/consumer.py +++ b/packages/modules/consumers/lambda_/lambda_/consumer.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 from pymodbus.constants import Endian import logging +from typing import Optional from modules.common.abstract_consumer import CurrentValues from modules.common.abstract_device import DeviceDescriptor @@ -15,8 +16,8 @@ def create_consumer(config: Lambda): - client = None - sim_counter = None + client: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None def initializer(): nonlocal client, sim_counter diff --git a/packages/modules/consumers/my_pv/acthor/config.py b/packages/modules/consumers/my_pv/acthor/config.py index 2e8f785058..4eb47c74c3 100644 --- a/packages/modules/consumers/my_pv/acthor/config.py +++ b/packages/modules/consumers/my_pv/acthor/config.py @@ -10,10 +10,10 @@ class ActhorConfiguration: def __init__(self, ip_address: Optional[str] = None, - port: Optional[int] = 502, - modbus_id: Optional[int] = 1, - model: Optional[str] = "9s45", - max_power: Optional[int] = 1000): + port: int = 502, + modbus_id: int = 1, + model: str = "9s45", + max_power: int = 1000): self.ip_address = ip_address self.port = port self.modbus_id = modbus_id @@ -27,7 +27,7 @@ def __init__(self, name: str = "my-PV Acthor oder Elwa2 Heizstab", type: str = "acthor", id: int = 0, - configuration: ActhorConfiguration = None, + configuration: Optional[ActhorConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_TUNABLE, ConsumerUsage.METER_ONLY), **kwargs) -> None: diff --git a/packages/modules/consumers/my_pv/acthor/consumer.py b/packages/modules/consumers/my_pv/acthor/consumer.py index f35e63cab7..914f449abf 100644 --- a/packages/modules/consumers/my_pv/acthor/consumer.py +++ b/packages/modules/consumers/my_pv/acthor/consumer.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 from enum import IntEnum 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 @@ -38,8 +39,8 @@ class Register(IntEnum): def create_consumer(config: Acthor): - client = None - sim_counter = None + client: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None def initializer(): nonlocal client, sim_counter diff --git a/packages/modules/consumers/my_pv/elwa_e/config.py b/packages/modules/consumers/my_pv/elwa_e/config.py index c41b65881f..8aaad7f8e7 100644 --- a/packages/modules/consumers/my_pv/elwa_e/config.py +++ b/packages/modules/consumers/my_pv/elwa_e/config.py @@ -10,8 +10,8 @@ class ElwaConfiguration: def __init__(self, ip_address: Optional[str] = None, - port: Optional[int] = 502, - modbus_id: Optional[int] = 1): + port: int = 502, + modbus_id: int = 1): self.ip_address = ip_address self.port = port self.modbus_id = modbus_id @@ -23,7 +23,7 @@ def __init__(self, name: str = "my-PV Elwa-E Heizstab", type: str = "elwa_e", id: int = 0, - configuration: ElwaConfiguration = None, + configuration: Optional[ElwaConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_TUNABLE, ConsumerUsage.METER_ONLY), **kwargs) -> None: diff --git a/packages/modules/consumers/my_pv/elwa_e/consumer.py b/packages/modules/consumers/my_pv/elwa_e/consumer.py index f5c7cfd598..1943178dba 100644 --- a/packages/modules/consumers/my_pv/elwa_e/consumer.py +++ b/packages/modules/consumers/my_pv/elwa_e/consumer.py @@ -2,6 +2,7 @@ from enum import IntEnum import logging from modules.common.abstract_device import DeviceDescriptor +from typing import Optional from modules.common.component_state import ConsumerState from modules.common.component_type import ComponentType from modules.common.configurable_consumer import ConfigurableConsumer, SetLimitData @@ -15,16 +16,16 @@ class Register(IntEnum): POWER = 1000 TEMP0 = 1001 - STATUS = 1003 + STATE = 1003 REG_MAPPING = ( (Register.POWER, [ModbusDataType.INT_16]), (Register.TEMP0, [ModbusDataType.INT_16]), - (Register.STATUS, ModbusDataType.INT_16), + (Register.STATE, ModbusDataType.INT_16), ) -STATUS = { +STATE_MAPPING = { 2: "Heat", 3: "Standby", 4: "Boost heat", @@ -39,11 +40,11 @@ class Register(IntEnum): def create_consumer(config: Elwa): - client = None + client: Optional[ModbusTcpClient_] = None fuse = 1 power = 0 - sim_counter = None - status = None + sim_counter: Optional[SimCounterConsumer] = None + state = None def initializer(): nonlocal client, fuse, sim_counter @@ -55,13 +56,13 @@ def error_handler() -> None: initializer() def update() -> ConsumerState: - nonlocal power, status + nonlocal power, state resp = client.read_holding_registers_bulk( Register.POWER, 4, mapping=REG_MAPPING, unit=config.configuration.modbus_id) power = resp[Register.POWER] - status = resp[Register.STATUS] - if status > 200: - raise Exception(f"Elwa-E meldet einen Fehler-Status: {STATUS.get(status, 'Unknown status')}") + state = resp[Register.STATE] + if state > 200: + raise Exception(f"Elwa-E meldet einen Fehler-Status: {STATE_MAPPING.get(state, 'Unknown fault state')}") imported, exported = sim_counter.sim_count(power) return ConsumerState( power=power, @@ -71,7 +72,7 @@ def update() -> ConsumerState: ) def set_power_limit(power_limit: float, data: SetLimitData) -> None: - if status == 4: + if state == 4: log.debug("Elwa-E im Boost-Heat Modus, keine Leistungsvorgabe möglich") return if power_limit < power: diff --git a/packages/modules/consumers/mystrom/mystrom/config.py b/packages/modules/consumers/mystrom/mystrom/config.py index 3532dff044..137cea1fe8 100644 --- a/packages/modules/consumers/mystrom/mystrom/config.py +++ b/packages/modules/consumers/mystrom/mystrom/config.py @@ -18,7 +18,7 @@ def __init__(self, name: str = "MyStrom", type: str = "mystrom", id: int = 0, - configuration: MyStromConfiguration = None, + configuration: Optional[MyStromConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_ONOFF, ConsumerUsage.METER_ONLY), **kwargs) -> None: diff --git a/packages/modules/consumers/mystrom/mystrom/consumer.py b/packages/modules/consumers/mystrom/mystrom/consumer.py index 82b07b13ad..4d7ab16166 100644 --- a/packages/modules/consumers/mystrom/mystrom/consumer.py +++ b/packages/modules/consumers/mystrom/mystrom/consumer.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import logging from modules.common import req +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 @@ -12,8 +13,8 @@ def create_consumer(config: MyStrom): - session = None - sim_counter = None + session: Optional[req.CustomSession] = None + sim_counter: Optional[SimCounterConsumer] = None def initializer(): nonlocal session, sim_counter diff --git a/packages/modules/consumers/nibe/nibe_s_series/config.py b/packages/modules/consumers/nibe/nibe_s_series/config.py index 5168d0de78..9d0ebabe04 100644 --- a/packages/modules/consumers/nibe/nibe_s_series/config.py +++ b/packages/modules/consumers/nibe/nibe_s_series/config.py @@ -10,7 +10,7 @@ class NibeConfiguration: def __init__(self, ip_address: Optional[str] = None, - modbus_id: Optional[int] = 1): + modbus_id: int = 1): self.ip_address = ip_address self.modbus_id = modbus_id @@ -21,7 +21,7 @@ def __init__(self, name: str = "Nibe S-Series Wärmepumpe", type: str = "nibe_s_series", id: int = 0, - configuration: NibeConfiguration = None, + configuration: Optional[NibeConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.METER_ONLY,), **kwargs) -> None: super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( diff --git a/packages/modules/consumers/nibe/nibe_s_series/consumer.py b/packages/modules/consumers/nibe/nibe_s_series/consumer.py index bf2142caf5..8357cff01f 100644 --- a/packages/modules/consumers/nibe/nibe_s_series/consumer.py +++ b/packages/modules/consumers/nibe/nibe_s_series/consumer.py @@ -1,5 +1,6 @@ #!/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 @@ -12,8 +13,8 @@ def create_consumer(config: Nibe): - client = None - sim_counter = None + client: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None def initializer(): nonlocal client, sim_counter diff --git a/packages/modules/consumers/ovum/ovum/config.py b/packages/modules/consumers/ovum/ovum/config.py index d37f948158..a9714c535a 100644 --- a/packages/modules/consumers/ovum/ovum/config.py +++ b/packages/modules/consumers/ovum/ovum/config.py @@ -10,8 +10,8 @@ class OvumConfiguration: def __init__(self, ip_address: Optional[str] = None, - port: Optional[int] = 502, - modbus_id: Optional[int] = 1): + port: int = 502, + modbus_id: int = 1): self.ip_address = ip_address self.port = port self.modbus_id = modbus_id @@ -23,7 +23,7 @@ def __init__(self, name: str = "OVUM Wärmepumpe (CubeSpeicher/MPlus)", type: str = "ovum", id: int = 0, - configuration: OvumConfiguration = None, + configuration: Optional[OvumConfiguration] = None, # OVUM unterstützt eine echte Leistungsvorgabe (SUSPENDABLE_TUNABLE), # Eigenregelung anhand der Systemwerte (SELF_CONTROLLED) sowie # SG-Ready-Ein-/Ausschalten (SUSPENDABLE_ONOFF) diff --git a/packages/modules/consumers/ovum/ovum/consumer.py b/packages/modules/consumers/ovum/ovum/consumer.py index 3961f4ac53..34299c457f 100644 --- a/packages/modules/consumers/ovum/ovum/consumer.py +++ b/packages/modules/consumers/ovum/ovum/consumer.py @@ -15,8 +15,8 @@ def create_consumer(config: Ovum): - client = None - sim_counter = None + client: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None def initializer(): nonlocal client, sim_counter diff --git a/packages/modules/consumers/ratiotherm/ratiotherm/config.py b/packages/modules/consumers/ratiotherm/ratiotherm/config.py index 4c325d0500..dc0965610e 100644 --- a/packages/modules/consumers/ratiotherm/ratiotherm/config.py +++ b/packages/modules/consumers/ratiotherm/ratiotherm/config.py @@ -10,8 +10,8 @@ class RatiothermConfiguration: def __init__(self, ip_address: Optional[str] = None, - port: Optional[int] = 502, - modbus_id: Optional[int] = 1): + port: int = 502, + modbus_id: int = 1): self.ip_address = ip_address self.port = port self.modbus_id = modbus_id @@ -23,7 +23,7 @@ def __init__(self, name: str = "Ratiotherm Wärmepumpe", type: str = "ratiotherm", id: int = 0, - configuration: RatiothermConfiguration = None, + configuration: Optional[RatiothermConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_TUNABLE,), **kwargs) -> None: super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( diff --git a/packages/modules/consumers/ratiotherm/ratiotherm/consumer.py b/packages/modules/consumers/ratiotherm/ratiotherm/consumer.py index 0ab039c769..fd35d4410b 100644 --- a/packages/modules/consumers/ratiotherm/ratiotherm/consumer.py +++ b/packages/modules/consumers/ratiotherm/ratiotherm/consumer.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import logging +from typing import Optional from modules.common.abstract_device import DeviceDescriptor from modules.common.component_type import ComponentType from modules.common.configurable_consumer import ConfigurableConsumer, SetLimitData @@ -11,8 +12,8 @@ def create_consumer(config: Ratiotherm): - client = None - sim_counter = None + client: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None def initializer(): nonlocal client, sim_counter @@ -26,6 +27,7 @@ def set_power_limit(power_limit: float, data: SetLimitData) -> None: # Absturz bei negativen Zahlen raw_value = int(round(max(power_limit, 0))) client.write_register(100, raw_value, ModbusDataType.INT_16, unit=config.configuration.modbus_id) + return ConfigurableConsumer(consumer_config=config, initializer=initializer, error_handler=error_handler, diff --git a/packages/modules/consumers/shelly/shelly_em/config.py b/packages/modules/consumers/shelly/shelly_em/config.py index 2d3169acfa..78352042e4 100644 --- a/packages/modules/consumers/shelly/shelly_em/config.py +++ b/packages/modules/consumers/shelly/shelly_em/config.py @@ -10,8 +10,8 @@ class ShellyConfiguration: def __init__(self, ip_address: Optional[str] = None, - factor: Optional[int] = -1, - phase: Optional[int] = 1, + factor: int = -1, + phase: int = 1, username: Optional[str] = None, password: Optional[str] = None) -> None: self.ip_address = ip_address @@ -27,7 +27,7 @@ def __init__(self, name: str = "Shelly EM (Messen)", type: str = "shelly_em", id: int = 0, - configuration: ShellyConfiguration = None, + configuration: Optional[ShellyConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.METER_ONLY,), **kwargs) -> None: super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( diff --git a/packages/modules/consumers/shelly/shelly_em/consumer.py b/packages/modules/consumers/shelly/shelly_em/consumer.py index f7e83e4cc9..29f411cc2d 100644 --- a/packages/modules/consumers/shelly/shelly_em/consumer.py +++ b/packages/modules/consumers/shelly/shelly_em/consumer.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 from modules.common.abstract_device import DeviceDescriptor +from typing import Optional from modules.common.component_state import ConsumerState from modules.common.component_type import ComponentType from modules.common.configurable_consumer import ConfigurableConsumer @@ -9,8 +10,8 @@ def create_consumer(config: ShellyEM): - sim_counter = None - generation = 1 + sim_counter: Optional[SimCounterConsumer] = None + generation: int = 1 def initializer(): nonlocal sim_counter, generation diff --git a/packages/modules/consumers/shelly/shelly_pm/config.py b/packages/modules/consumers/shelly/shelly_pm/config.py index d0676e9a84..09dfb24790 100644 --- a/packages/modules/consumers/shelly/shelly_pm/config.py +++ b/packages/modules/consumers/shelly/shelly_pm/config.py @@ -10,8 +10,8 @@ class ShellyConfiguration: def __init__(self, ip_address: Optional[str] = None, - factor: Optional[int] = -1, - phase: Optional[int] = 1, + factor: int = -1, + phase: int = 1, channel: int = 0, username: Optional[str] = None, password: Optional[str] = None) -> None: @@ -29,7 +29,7 @@ def __init__(self, name: str = "Shelly PM (Messen & Schalten)", type: str = "shelly_pm", id: int = 0, - configuration: ShellyConfiguration = None, + configuration: Optional[ShellyConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.METER_ONLY, ConsumerUsage.SUSPENDABLE_ONOFF), **kwargs) -> None: diff --git a/packages/modules/consumers/shelly/shelly_pm/consumer.py b/packages/modules/consumers/shelly/shelly_pm/consumer.py index db4ac15646..cb5940323a 100644 --- a/packages/modules/consumers/shelly/shelly_pm/consumer.py +++ b/packages/modules/consumers/shelly/shelly_pm/consumer.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 from modules.common import req +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 @@ -10,8 +11,9 @@ def create_consumer(config: ShellyPM): - sim_counter = model = None - generation = 1 + sim_counter: Optional[SimCounterConsumer] = None + model: Optional[str] = None + generation: int = 1 def initializer(): nonlocal sim_counter, generation, model diff --git a/packages/modules/consumers/solarfocus/vampair/config.py b/packages/modules/consumers/solarfocus/vampair/config.py index 0de42b9fba..8b6a750328 100644 --- a/packages/modules/consumers/solarfocus/vampair/config.py +++ b/packages/modules/consumers/solarfocus/vampair/config.py @@ -8,7 +8,7 @@ @auto_str class VampairConfiguration: - def __init__(self, ip_address: Optional[str] = None, port: Optional[int] = 502, modbus_id: Optional[int] = 1): + 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 @@ -20,7 +20,7 @@ def __init__(self, name: str = "Vampair Wärmepumpe", type: str = "vampair", id: int = 0, - configuration: VampairConfiguration = None, + configuration: Optional[VampairConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SELF_CONTROLLED, ConsumerUsage.SUSPENDABLE_TUNABLE, ConsumerUsage.METER_ONLY), diff --git a/packages/modules/consumers/solarfocus/vampair/consumer.py b/packages/modules/consumers/solarfocus/vampair/consumer.py index 812571a634..a8fc735f15 100644 --- a/packages/modules/consumers/solarfocus/vampair/consumer.py +++ b/packages/modules/consumers/solarfocus/vampair/consumer.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +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 @@ -10,8 +11,8 @@ def create_consumer(config: Vampair): - client = None - sim_counter = None + client: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None def initializer(): nonlocal client, sim_counter diff --git a/packages/modules/consumers/solvis/solvis/config.py b/packages/modules/consumers/solvis/solvis/config.py index 564832c8da..e172838476 100644 --- a/packages/modules/consumers/solvis/solvis/config.py +++ b/packages/modules/consumers/solvis/solvis/config.py @@ -8,7 +8,7 @@ @auto_str class SolvisConfiguration: - def __init__(self, ip_address: Optional[str] = None, port: Optional[int] = 502, modbus_id: Optional[int] = 1): + 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 @@ -20,7 +20,7 @@ def __init__(self, name: str = "Solvis Wärmepumpe", type: str = "solvis", id: int = 0, - configuration: SolvisConfiguration = None, + configuration: Optional[SolvisConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.METER_ONLY,), **kwargs) -> None: super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( diff --git a/packages/modules/consumers/solvis/solvis/consumer.py b/packages/modules/consumers/solvis/solvis/consumer.py index 3830a9d2f4..00d976092c 100644 --- a/packages/modules/consumers/solvis/solvis/consumer.py +++ b/packages/modules/consumers/solvis/solvis/consumer.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +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 @@ -9,8 +10,8 @@ def create_consumer(config: SolvisHeatPump): - client = None - sim_counter = None + client: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None def initializer(): nonlocal client, sim_counter diff --git a/packages/modules/consumers/stiebel/stiebel/config.py b/packages/modules/consumers/stiebel/stiebel/config.py index aa940eef11..fad44c909a 100644 --- a/packages/modules/consumers/stiebel/stiebel/config.py +++ b/packages/modules/consumers/stiebel/stiebel/config.py @@ -10,8 +10,8 @@ class StiebelConfiguration: def __init__(self, ip_address: Optional[str] = None, - port: Optional[int] = 502, - modbus_id: Optional[int] = 1): + port: int = 502, + modbus_id: int = 1): self.ip_address = ip_address self.port = port self.modbus_id = modbus_id @@ -23,7 +23,7 @@ def __init__(self, name: str = "Stiebel", type: str = "stiebel", id: int = 0, - configuration: StiebelConfiguration = None, + configuration: Optional[StiebelConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_ONOFF,), **kwargs) -> None: super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( diff --git a/packages/modules/consumers/stiebel/stiebel/consumer.py b/packages/modules/consumers/stiebel/stiebel/consumer.py index c7d39a3a1e..59ee1b3323 100644 --- a/packages/modules/consumers/stiebel/stiebel/consumer.py +++ b/packages/modules/consumers/stiebel/stiebel/consumer.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import logging +from typing import Optional from modules.common.abstract_device import DeviceDescriptor from modules.common.component_type import ComponentType from modules.common.configurable_consumer import ConfigurableConsumer @@ -11,8 +12,8 @@ def create_consumer(config: Stiebel): - client = None - sim_counter = None + client: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None def initializer(): nonlocal client, sim_counter diff --git a/packages/modules/consumers/viessmann/viessmann_heat_pump/config.py b/packages/modules/consumers/viessmann/viessmann_heat_pump/config.py index 6274b60698..f4618f14cb 100644 --- a/packages/modules/consumers/viessmann/viessmann_heat_pump/config.py +++ b/packages/modules/consumers/viessmann/viessmann_heat_pump/config.py @@ -8,7 +8,7 @@ @auto_str class ViessmannConfiguration: - def __init__(self, ip_address: Optional[str] = None, port: Optional[int] = 502, modbus_id: Optional[int] = 1): + 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 @@ -20,7 +20,7 @@ def __init__(self, name: str = "Viessmann Wärmepumpe", type: str = "viessmann_heat_pump", id: int = 0, - configuration: ViessmannConfiguration = None, + configuration: Optional[ViessmannConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_ONOFF,), **kwargs) -> None: super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory( diff --git a/packages/modules/consumers/viessmann/viessmann_heat_pump/consumer.py b/packages/modules/consumers/viessmann/viessmann_heat_pump/consumer.py index cac0876d05..1837dd8784 100644 --- a/packages/modules/consumers/viessmann/viessmann_heat_pump/consumer.py +++ b/packages/modules/consumers/viessmann/viessmann_heat_pump/consumer.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +from typing import Optional from modules.common.abstract_device import DeviceDescriptor from modules.common.configurable_consumer import ConfigurableConsumer from modules.common.modbus import ModbusTcpClient_ @@ -6,7 +7,7 @@ def create_consumer(config: ViessmannHeatPump): - client = None + client: Optional[ModbusTcpClient_] = None def initializer(): nonlocal client diff --git a/packages/modules/devices/shelly/shelly/status_handler.py b/packages/modules/devices/shelly/shelly/status_handler.py index 5ea1db2154..0dcb1a6df3 100644 --- a/packages/modules/devices/shelly/shelly/status_handler.py +++ b/packages/modules/devices/shelly/shelly/status_handler.py @@ -9,7 +9,7 @@ ALPHABETICAL_INDEX = ['a', 'b', 'c'] -def get_generation(address: str) -> Tuple[Optional[int], str]: +def get_generation(address: str) -> Tuple[int, str]: device_info = req.get_http_session().get(f"http://{address}/shelly", timeout=3).json() generation = 1 # default to gen 1 model = "unknown"