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..5d7939788d --- /dev/null +++ b/docs/samples/sample_consumer/sample_consumer/consumer.py @@ -0,0 +1,88 @@ +#!/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.component_type import ComponentType +from modules.common.configurable_consumer import ConfigurableConsumer, SetLimitData +from modules.common.modbus import ModbusDataType, ModbusTcpClient_ +from modules.common.simcount._simcounter import SimCounterConsumer +from modules.consumers.sample.sample.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_power_limit(power_limit: float, data: SetLimitData) -> 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_power_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..e2703fe704 --- /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: int = 502, + modbus_id: 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: Optional[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..8ec2495e4b --- /dev/null +++ b/packages/modules/consumers/askoma/askoheat/consumer.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +import logging +from typing import Optional +from modules.common.abstract_device import DeviceDescriptor +from modules.common.component_state import ConsumerState +from modules.common.component_type import ComponentType +from modules.common.configurable_consumer import ConfigurableConsumer, SetLimitData +from modules.common.modbus import ModbusDataType, ModbusTcpClient_ +from modules.common.simcount._simcounter import SimCounterConsumer +from modules.consumers.askoma.askoheat.config import Askoheat + +log = logging.getLogger(__name__) + + +def create_consumer(config: Askoheat): + client: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None + + def initializer(): + nonlocal client, sim_counter + client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + + def 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_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_power_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..b0adccac5b --- /dev/null +++ b/packages/modules/consumers/avm/avm/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 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[float] = 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: Optional[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..1b32aaa5bf --- /dev/null +++ b/packages/modules/consumers/avm/avm/consumer.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +import hashlib +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 +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: Optional[str] = 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 + ) + 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: + 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..2d2b847095 --- /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: int = 502, + modbus_id: 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: Optional[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..c0777cb960 --- /dev/null +++ b/packages/modules/consumers/generic/dac/consumer.py @@ -0,0 +1,49 @@ +#!/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 +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 + +log = logging.getLogger(__name__) + + +def create_consumer(config: Dac): + client: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None + + def initializer(): + nonlocal client, sim_counter + client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + + def error_handler() -> None: + initializer() + + 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_power_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..83840f9448 --- /dev/null +++ b/packages/modules/consumers/generic/http/config.py @@ -0,0 +1,48 @@ +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 +from ..vendor import vendor_descriptor + + +@auto_str +class HttpConfiguration: + def __init__(self, + 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 + 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: Optional[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..ae7becc93f --- /dev/null +++ b/packages/modules/consumers/generic/http/consumer.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +import logging +from typing import Optional, Callable + +from modules.common import req +from modules.common.abstract_device import DeviceDescriptor +from modules.common.component_state import ConsumerState +from modules.common.component_type import ComponentType +from modules.common.configurable_consumer import ConfigurableConsumer, SetLimitData +from modules.common.simcount._simcounter import SimCounterConsumer +from modules.consumers.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: 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 + 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, {"state": True}) + + def switch_off(): + # Authorization? + post_switch_off(session, {"state": False}) + + def set_power_limit(power_limit: float, data: SetLimitData) -> None: + # Authorization? + post_set_power_limit(session, {"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..708ca6e3cf --- /dev/null +++ b/packages/modules/consumers/generic/json/config.py @@ -0,0 +1,47 @@ +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 +from ..vendor import vendor_descriptor + + +@auto_str +class JsonConfiguration: + def __init__(self, + 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 + 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: Optional[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..29676e13e3 --- /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, SetLimitData +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: Optional[req.CustomSession] = None + sim_counter: Optional[SimCounterConsumer] = 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() -> 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 + 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({"state": True}) + + def switch_off(): + # Authorization? + jq_switch_off({"state": False}) + + def set_power_limit(power_limit: float, data: SetLimitData) -> None: + # Authorization? + jq_set_power_limit({"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..5526ad04f8 --- /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: int = 502, + modbus_id: 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: Optional[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..8f50b9b19c --- /dev/null +++ b/packages/modules/consumers/idm/idm/consumer.py @@ -0,0 +1,91 @@ +#!/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 +from modules.common.component_state import ConsumerState +from modules.common.component_type import ComponentType +from modules.common.configurable_consumer import ConfigurableConsumer, SetLimitData +from modules.common.modbus import ModbusDataType, ModbusTcpClient_ +from modules.common.simcount._simcounter import SimCounterConsumer +from modules.consumers.idm.idm.config import Idm + +log = logging.getLogger(__name__) + + +def create_consumer(config: Idm): + client: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None + + def initializer(): + nonlocal client, sim_counter + client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + + def 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_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 + # 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_power_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..da06fddf4b --- /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: int = 502, + modbus_id: 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: Optional[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..6c8cf7ba0e --- /dev/null +++ b/packages/modules/consumers/lambda_/lambda_/consumer.py @@ -0,0 +1,65 @@ +#!/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 +from modules.common.component_state import ConsumerState +from modules.common.component_type import ComponentType +from modules.common.configurable_consumer import ConfigurableConsumer, SetLimitData +from modules.common.modbus import ModbusDataType, ModbusTcpClient_ +from modules.common.simcount._simcounter import SimCounterConsumer +from modules.consumers.lambda_.lambda_.config import Lambda + +log = logging.getLogger(__name__) + + +def create_consumer(config: Lambda): + client: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None + + def initializer(): + nonlocal client, sim_counter + client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + + def 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_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, + 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_power_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..4eb47c74c3 --- /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: 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 + 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: Optional[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..914f449abf --- /dev/null +++ b/packages/modules/consumers/my_pv/acthor/consumer.py @@ -0,0 +1,75 @@ +#!/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 +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 + +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: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None + + def initializer(): + nonlocal client, sim_counter + client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + + def 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_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_power_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..8aaad7f8e7 --- /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: int = 502, + modbus_id: 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: Optional[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..1943178dba --- /dev/null +++ b/packages/modules/consumers/my_pv/elwa_e/consumer.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +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 +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 + STATE = 1003 + + +REG_MAPPING = ( + (Register.POWER, [ModbusDataType.INT_16]), + (Register.TEMP0, [ModbusDataType.INT_16]), + (Register.STATE, ModbusDataType.INT_16), +) + +STATE_MAPPING = { + 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: Optional[ModbusTcpClient_] = None + fuse = 1 + power = 0 + sim_counter: Optional[SimCounterConsumer] = None + state = 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 power, state + resp = client.read_holding_registers_bulk( + Register.POWER, 4, mapping=REG_MAPPING, unit=config.configuration.modbus_id) + power = resp[Register.POWER] + 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, + imported=imported, + exported=exported, + temperatures=[resp[Register.TEMP0]/10] + ) + + def set_power_limit(power_limit: float, data: SetLimitData) -> None: + if state == 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 = max(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_power_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..137cea1fe8 --- /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: Optional[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..4d7ab16166 --- /dev/null +++ b/packages/modules/consumers/mystrom/mystrom/consumer.py @@ -0,0 +1,56 @@ +#!/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 +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: Optional[req.CustomSession] = None + sim_counter: Optional[SimCounterConsumer] = 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..9d0ebabe04 --- /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: 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: Optional[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..8357cff01f --- /dev/null +++ b/packages/modules/consumers/nibe/nibe_s_series/consumer.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +import logging +from typing import Optional +from modules.common.abstract_device import DeviceDescriptor +from modules.common.component_state import ConsumerState +from modules.common.component_type import ComponentType +from modules.common.configurable_consumer import ConfigurableConsumer +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: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = 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..a9714c535a --- /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: int = 502, + modbus_id: 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: Optional[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..34299c457f --- /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, SetLimitData +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: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None + + def initializer(): + nonlocal client, sim_counter + client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + + def 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_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 + 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_power_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..dc0965610e --- /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: int = 502, + modbus_id: 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: Optional[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..fd35d4410b --- /dev/null +++ b/packages/modules/consumers/ratiotherm/ratiotherm/consumer.py @@ -0,0 +1,37 @@ +#!/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 +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: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None + + def initializer(): + nonlocal client, sim_counter + client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + + def error_handler() -> None: + initializer() + + 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, + set_power_limit=set_power_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..78352042e4 --- /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: int = -1, + phase: 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: Optional[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..29f411cc2d --- /dev/null +++ b/packages/modules/consumers/shelly/shelly_em/consumer.py @@ -0,0 +1,47 @@ +#!/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 +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: Optional[SimCounterConsumer] = None + generation: int = 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..09dfb24790 --- /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: int = -1, + phase: 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: Optional[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..cb5940323a --- /dev/null +++ b/packages/modules/consumers/shelly/shelly_pm/consumer.py @@ -0,0 +1,84 @@ +#!/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 +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: Optional[SimCounterConsumer] = None + model: Optional[str] = None + generation: int = 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/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..8b6a750328 --- /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: int = 502, modbus_id: 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: Optional[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..a8fc735f15 --- /dev/null +++ b/packages/modules/consumers/solarfocus/vampair/consumer.py @@ -0,0 +1,51 @@ +#!/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 +from modules.common.component_type import ComponentType +from modules.common.configurable_consumer import ConfigurableConsumer, SetLimitData +from modules.common.modbus import ModbusDataType, ModbusTcpClient_ +from modules.common.simcount._simcounter import SimCounterConsumer +from modules.consumers.solarfocus.vampair.config import Vampair + + +def create_consumer(config: Vampair): + client: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None + + def initializer(): + nonlocal client, sim_counter + client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + + def 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, 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) + + 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) 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..e172838476 --- /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: int = 502, modbus_id: 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: Optional[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..00d976092c --- /dev/null +++ b/packages/modules/consumers/solvis/solvis/consumer.py @@ -0,0 +1,39 @@ +#!/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 +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: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None + + def initializer(): + nonlocal client, sim_counter + client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + + def 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..fad44c909a --- /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: int = 502, + modbus_id: 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: Optional[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..59ee1b3323 --- /dev/null +++ b/packages/modules/consumers/stiebel/stiebel/consumer.py @@ -0,0 +1,40 @@ +#!/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 +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: Optional[ModbusTcpClient_] = None + sim_counter: Optional[SimCounterConsumer] = None + + def initializer(): + nonlocal client, sim_counter + client = ModbusTcpClient_(config.configuration.ip_address, config.configuration.port) + sim_counter = SimCounterConsumer(config.id, ComponentType.CONSUMER) + + def 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..f4618f14cb --- /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: int = 502, modbus_id: 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: Optional[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..1837dd8784 --- /dev/null +++ b/packages/modules/consumers/viessmann/viessmann_heat_pump/consumer.py @@ -0,0 +1,32 @@ +#!/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_ +from modules.consumers.viessmann.viessmann_heat_pump.config import ViessmannHeatPump + + +def create_consumer(config: ViessmannHeatPump): + client: Optional[ModbusTcpClient_] = 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) 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"