diff --git a/modules/bezug_e3dc/e3dc.py b/modules/bezug_e3dc/e3dc.py deleted file mode 100755 index eff90a573..000000000 --- a/modules/bezug_e3dc/e3dc.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/python -import logging -from typing import List - -from pymodbus.constants import Endian - -from helpermodules.cli import run_using_positional_cli_args -from modules.common.component_state import CounterState -from modules.common.modbus import ModbusTcpClient_, ModbusDataType -from modules.common.simcount import sim_count -from modules.common.store import get_counter_value_store - -log = logging.getLogger("E3DC EVU") - - -def update(ipaddress: str): - log.debug("Beginning update") - with ModbusTcpClient_(ipaddress, port=502) as client: - # 40074 EVU Punkt negativ -> Einspeisung in Watt - power_all = client.read_holding_registers(40073, ModbusDataType.INT_32, wordorder=Endian.Little, unit=1) - # 40130 Phasenleistung in Watt - # max 6 Leistungsmesser verbaut ab 410105, typ 1 ist evu - # bei den meisten e3dc auf 40128 - # for i in range (40104,40132,4): - for i in range(40128, 40103, -4): - # powers = client.read_holding_registers(40129, [ModbusDataType.INT_16] * 3, unit=1) - powers = client.read_holding_registers(i, [ModbusDataType.INT_16] * 4, unit=1) - log.debug("I: %d, p[0] typ %d p[1] a1 %d p[2] a2 %d p[3] a3 %d", - i, powers[0], powers[1], powers[2], powers[3]) - if powers[0] == 1: - log.debug("Evu Leistungsmessung gefunden") - break - counter_import, counter_export = sim_count(power_all, prefix="bezug") - get_counter_value_store(1).set(CounterState( - imported=counter_import, - exported=counter_export, - power=power_all, - powers=powers[1:] - )) - log.debug("Update completed successfully") - - -def main(argv: List[str]): - run_using_positional_cli_args(update, argv) diff --git a/modules/bezug_e3dc/main.sh b/modules/bezug_e3dc/main.sh index e07b72d7e..313fb10b8 100755 --- a/modules/bezug_e3dc/main.sh +++ b/modules/bezug_e3dc/main.sh @@ -10,7 +10,7 @@ else MYLOGFILE="${RAMDISKDIR}/evu.log" fi -bash "$OPENWBBASEDIR/packages/legacy_run.sh" "bezug_e3dc.e3dc" "${e3dcip}" >>"${MYLOGFILE}" 2>&1 +bash "$OPENWBBASEDIR/packages/legacy_run.sh" "modules.e3dc.device" "counter" "$e3dcip" "$e3dc2ip" "$e3dcextprod" "$pvwattmodul" "1">>"${MYLOGFILE}" 2>&1 ret=$? openwbDebugLog ${DMOD} 2 "EVU RET: ${ret}" diff --git a/modules/speicher_e3dc/e3dc.py b/modules/speicher_e3dc/e3dc.py deleted file mode 100755 index 8ebf01d9f..000000000 --- a/modules/speicher_e3dc/e3dc.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/python3 -import logging -from typing import Iterable, List - -from pymodbus.constants import Endian - -from helpermodules.cli import run_using_positional_cli_args -from modules.common.component_context import SingleComponentUpdateContext -from modules.common.component_state import InverterState, BatState -from modules.common.fault_state import ComponentInfo -from modules.common.modbus import ModbusTcpClient_, ModbusDataType -from modules.common.simcount import sim_count -from modules.common.store import get_inverter_value_store, get_bat_value_store -from modules.common.store.ramdisk import files - -log = logging.getLogger("E3DC Battery") - - -def update_e3dc_battery(addresses: Iterable[str], read_external: int, pv_other: bool): - soc = 0 - count = 0 - battery_power = 0 - # pv_external - > pv Leistung die als externe Produktion an e3dc angeschlossen ist - # nur auslesen wenn als relevant parametrisiert (read_external = 1) , sonst doppelte Auslesung - pv_external = 0 - # pv -> pv Leistung die direkt an e3dc angeschlossen ist - pv = 0 - for address in addresses: - log.debug("Battery Ip: %s, read_external %d pv_other %s", address, read_external, pv_other) - count += 1 - with ModbusTcpClient_(address, port=502) as client: - # 40082 SoC - soc += client.read_holding_registers(40082, ModbusDataType.INT_16, unit=1) - # 40069 Speicherleistung - battery_power += client.read_holding_registers(40069, - ModbusDataType.INT_32, wordorder=Endian.Little, unit=1) - # 40067 PV Leistung - pv += (client.read_holding_registers(40067, ModbusDataType.INT_32, wordorder=Endian.Little, unit=1) * -1) - if read_external == 1: - # 40075 externe PV Leistung - pv_external += client.read_holding_registers(40075, - ModbusDataType.INT_32, wordorder=Endian.Little, unit=1) - soc = soc / count - log.debug("Battery soc %d battery_power %d pv %d pv_external %d count ip %d", - soc, battery_power, pv, pv_external, count) - counter_import, counter_export = sim_count(battery_power, prefix="speicher") - get_bat_value_store(1).set(BatState(power=battery_power, soc=soc, imported=counter_import, exported=counter_export)) - # pv_other sagt aus, ob WR definiert ist, und dessen PV Leistung auch gilt - # wenn 0 gilt nur PV und pv_external aus e3dc - pv_total = pv + pv_external - # Wenn wr1 nicht definiert ist, gilt nur die PV Leistung die hier im Modul ermittelt wurde - # als gesamte PV Leistung für wr1 - if not pv_other or pv_total != 0: - # Wenn wr1 definiert ist, gilt die bestehende PV Leistung aus Wr1 und das was hier im Modul ermittelt wurde - # als gesamte PV Leistung für wr1 - if pv_other: - try: - pv_total = pv_total + files.pv[0].power.read() - except: - pass - log.debug("wr update pv_other %s pv_total %d", pv_other, pv_total) - _, counter_pv = sim_count(pv_total, prefix="pv") - get_inverter_value_store(1).set(InverterState(exported=counter_pv, power=pv_total)) - - -def update(address1: str, address2: str, read_external: int, pvmodul: str): - # read_external is 0 or 1 - log.debug("Beginning update") - addresses = [address for address in [address1, address2] if address != "none"] - pv_other = pvmodul != "none" - bat_info = ComponentInfo(None, "E3DC", "bat") - with SingleComponentUpdateContext(bat_info): - update_e3dc_battery(addresses, read_external, pv_other) - log.debug("Update completed successfully") - - -def main(argv: List[str]): - run_using_positional_cli_args(update, argv) diff --git a/modules/speicher_e3dc/main.sh b/modules/speicher_e3dc/main.sh index 3506bf455..454729382 100755 --- a/modules/speicher_e3dc/main.sh +++ b/modules/speicher_e3dc/main.sh @@ -10,7 +10,7 @@ else MYLOGFILE="${RAMDISKDIR}/bat.log" fi -bash "$OPENWBBASEDIR/packages/legacy_run.sh" "speicher_e3dc.e3dc" "$e3dcip" "$e3dc2ip" "$e3dcextprod" "$pvwattmodul" >>"${MYLOGFILE}" 2>&1 +bash "$OPENWBBASEDIR/packages/legacy_run.sh" "modules.e3dc.device" "batv19" "$e3dcip" "$e3dc2ip" "$e3dcextprod" "$pvwattmodul" "1">>"${MYLOGFILE}" 2>&1 ret=$? openwbDebugLog "${DMOD}" 2 "BAT RET: ${ret}" diff --git a/packages/modules/e3dc/bat.py b/packages/modules/e3dc/bat.py new file mode 100644 index 000000000..5272ffca3 --- /dev/null +++ b/packages/modules/e3dc/bat.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +import logging + +from modules.common import modbus +from modules.common.component_state import BatState +from modules.common.component_type import ComponentDescriptor +from modules.common.modbus import ModbusDataType, Endian +from modules.common.fault_state import ComponentInfo +from modules.common.store import get_bat_value_store +from modules.common.simcount._simcounter import SimCounter +from modules.e3dc.config import E3dcBatSetup + + +log = logging.getLogger(__name__) + + +def read_bat(client: modbus.ModbusTcpClient_) -> [int, int]: + # 40082 SoC + soc = client.read_holding_registers(40082, ModbusDataType.INT_16, unit=1) + # 40069 Speicherleistung + power = client.read_holding_registers(40069, ModbusDataType.INT_32, wordorder=Endian.Little, unit=1) + return soc, power + + +class E3dcBat: + def __init__(self, + device_id: int, + address: str, + component_config: E3dcBatSetup) -> None: + self.__device_id = device_id + self.component_config = component_config + self.__device_id = device_id + # bat + self.__sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="speicher") + self.__store = get_bat_value_store(self.component_config.id) + self.component_info = ComponentInfo.from_component_config(self.component_config) + self.__address = address + + def update(self, client: modbus.ModbusTcpClient_) -> None: + + soc, power = read_bat(client) + log.debug("Ip: %s, soc %d power %d", self.__address, + soc, power) + imported, exported = self.__sim_counter.sim_count(power) + bat_state = BatState( + power=power, + soc=soc, + imported=imported, + exported=exported + ) + self.__store.set(bat_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=E3dcBatSetup) diff --git a/packages/modules/e3dc/config.py b/packages/modules/e3dc/config.py new file mode 100644 index 000000000..9c0b0a62b --- /dev/null +++ b/packages/modules/e3dc/config.py @@ -0,0 +1,76 @@ +from modules.common.component_setup import ComponentSetup +from helpermodules.auto_str import auto_str + + +@auto_str +class E3dcConfiguration: + def __init__(self, address: str = None, + read_ext: int = 0, + pvmodul: str = None + ): + self.address = address + self.read_ext = read_ext + self.pvmodul = pvmodul + + +@auto_str +class E3dc: + def __init__(self, + name: str = "e3dc", + type: str = "e3dc", + id: int = 0, + configuration: E3dcConfiguration = None) -> None: + self.name = name + self.type = type + self.id = id + self.configuration = configuration or E3dcConfiguration() + + +@auto_str +class E3dcBatConfiguration: + def __init__(self): + pass + + +@auto_str +class E3dcBatSetup(ComponentSetup[E3dcBatConfiguration]): + def __init__(self, + name: str = "e3dc Speicher", + type: str = "bat", + id: int = 0, + configuration: E3dcBatConfiguration = None) -> None: + super().__init__(name, type, id, configuration + or E3dcBatConfiguration()) + + +@auto_str +class E3dcCounterConfiguration: + def __init__(self): + pass + + +@auto_str +class E3dcCounterSetup(ComponentSetup[E3dcCounterConfiguration]): + def __init__(self, + name: str = "e3dc Zähler", + type: str = "counter", + id: int = 0, + configuration: E3dcCounterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or + E3dcCounterConfiguration()) + + +@auto_str +class E3dcInverterConfiguration: + def __init__(self): + pass + + +@auto_str +class E3dcInverterSetup(ComponentSetup[E3dcInverterConfiguration]): + def __init__(self, + name: str = "E3dc Wechselrichter", + type: str = "inverter", + id: int = 0, + configuration: E3dcInverterConfiguration = None) -> None: + super().__init__(name, type, id, configuration or E3dcInverterConfiguration()) diff --git a/packages/modules/e3dc/counter.py b/packages/modules/e3dc/counter.py new file mode 100644 index 000000000..5185ddc7a --- /dev/null +++ b/packages/modules/e3dc/counter.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +import logging +from typing import Dict, Union + +from dataclass_utils import dataclass_from_dict +from modules.common import modbus +from modules.common.component_state import CounterState +from modules.common.component_type import ComponentDescriptor +from modules.common.fault_state import ComponentInfo +from modules.common.simcount._simcounter import SimCounter +from modules.common.modbus import ModbusDataType, Endian +from modules.common.store import get_counter_value_store +from modules.e3dc.config import E3dcCounterSetup + +log = logging.getLogger(__name__) + + +def read_counter(client: modbus.ModbusTcpClient_) -> [int, [int]]: + log.debug("Beginning EVU update") + power = client.read_holding_registers(40073, ModbusDataType.INT_32, wordorder=Endian.Little, unit=1) + # 40130,40131, 40132 je Phasenleistung in Watt + # max 6 Leistungsmesser verbaut ab 40105, typ 1 ist evu + # bei den meisten e3dc auf 40128 + # for register in range (40104,40132,4): + meters = client.read_holding_registers(40104, [ModbusDataType.INT_16] * 28, unit=1) + log.debug("power: %d, meters: %s", power, meters) + powers = next(meters[i+1:i+4] for i in reversed(range(0, len(meters), 4)) if meters[i] == 1) + log.debug("powers %s", powers) + return power, powers + + +class E3dcCounter: + def __init__(self, + device_id: int, + address: str, + component_config: Union[Dict, E3dcCounterSetup]) -> None: + self.__device_id = device_id + self.component_config = dataclass_from_dict(E3dcCounterSetup, component_config) + self.__sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="bezug") + self.__store = get_counter_value_store(self.component_config.id) + self.component_info = ComponentInfo.from_component_config(self.component_config) + self.__address = address + + def update(self, client: modbus.ModbusTcpClient_): + power, powers = read_counter(client) + log.debug("Ip: %s, power %d", self.__address, power) + imported, exported = self.__sim_counter.sim_count(power) + counter_state = CounterState( + imported=imported, + exported=exported, + powers=powers, + power=power + ) + self.__store.set(counter_state) + log.debug("Update completed successfully") + + +component_descriptor = ComponentDescriptor(configuration_factory=E3dcCounterSetup) diff --git a/packages/modules/e3dc/device.py b/packages/modules/e3dc/device.py new file mode 100644 index 000000000..91f2990e9 --- /dev/null +++ b/packages/modules/e3dc/device.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +import logging +from typing import List, Union, Iterable + +from helpermodules.cli import run_using_positional_cli_args +from modules.common.abstract_device import DeviceDescriptor +from modules.common.configurable_device import ConfigurableDevice, ComponentFactoryByType, SingleComponentUpdateContext +from modules.common import modbus +from modules.e3dc.bat import E3dcBat, read_bat +from modules.e3dc.inverter import E3dcInverter, read_inverter +from modules.e3dc.counter import E3dcCounter +from modules.e3dc.config import E3dc, E3dcConfiguration +from modules.e3dc.config import E3dcBatSetup +from modules.e3dc.config import E3dcCounterSetup, E3dcCounterConfiguration +from modules.e3dc.config import E3dcInverterSetup +from modules.common.store.ramdisk import files +from modules.common.simcount import sim_count +from modules.common.store import get_inverter_value_store, get_bat_value_store +from modules.common.component_state import InverterState, BatState + + +log = logging.getLogger(__name__) + + +def create_device(device_config: E3dc): + def create_bat_component(component_config: E3dcBatSetup): + return E3dcBat(device_config.id, + device_config.configuration.address, + component_config) + + def create_counter_component(component_config: E3dcCounterSetup): + return E3dcCounter(device_config.id, + device_config.configuration.address, + component_config) + + def create_inverter_component(component_config: E3dcInverterSetup): + return E3dcInverter(device_config.id, + device_config.configuration.address, + device_config.configuration.read_ext, + device_config.configuration.pvmodul, + component_config) + + def update_components(components: Iterable[Union[E3dcBat, E3dcCounter, E3dcInverter]]): + with modbus.ModbusTcpClient_(device_config.configuration.address, 502) as client: + for component in components: + with SingleComponentUpdateContext(component.component_info): + component.update(client) + + return ConfigurableDevice( + device_config=device_config, + component_factory=ComponentFactoryByType( + bat=create_bat_component, + counter=create_counter_component, + inverter=create_inverter_component, + ), + component_updater=update_components + ) + + +def run_device_legacy(device_config: E3dc, component_config: Union[E3dcBatSetup, E3dcCounterSetup, E3dcBatSetup]): + device = create_device(device_config) + device.add_component(component_config) + log.debug("E3dc Configuration: %s, Component Configuration: %s", device_config, component_config) + device.update() + + +def create_legacy_device_config(address: str, + read_ext: int, + pvmodul: str, + num: int) -> None: + device_config = E3dc(configuration=E3dcConfiguration(address=address, + read_ext=read_ext, + pvmodul=pvmodul), + id=num) + log.debug("Konfig: %s", device_config) + return device_config + + +def read_legacy_counter(address1: str, + address2: str, + read_ext: int, + pvmodul: str, + num: int): + component_config = E3dcCounterSetup(configuration=E3dcCounterConfiguration()) + component_config.id = num + run_device_legacy(create_legacy_device_config(address1, + read_ext, + pvmodul, + num), component_config) + + +def read_legacy_batv19(address1: str, + address2: str, read_ext: int, + pvmodul: str, + num: int) -> None: + # für openwbv19 können mit der bisherigen parametrisierung zwei ip_addressen + # ausgelesen werden + # ebenso wird bei Speicheraufruf beides (Speicher und PV ausgelesen + # in openwb v2.0 geht nur noch eine IP adresse und die Pv muss + # separate ausgelesen werden + addresses = [address for address in [address1, address2] if address != "none"] + log.debug('e3dc IP-Adresse1: ' + address1) + log.debug('e3dc IP-Adresse2: ' + address2) + log.debug('e3dc read_ext: ' + str(read_ext)) + log.debug('e3dc pvmodul: ' + pvmodul) + log.debug('e3dc id: ' + str(num)) + soc = 0 + power = 0 + pv_external = 0 + pv = 0 + pv_other = pvmodul != "none" + for address in addresses: + log.debug("Ip: %s, read_external %d pv_other %s", address, read_ext, pv_other) + with modbus.ModbusTcpClient_(address, port=502) as client: + soc_tmp, power_tmp = read_bat(client) + soc += soc_tmp + power += power_tmp + pv_tmp, pv_external_tmp = read_inverter(client, read_ext) + pv += pv_tmp + pv_external += pv_external_tmp + soc /= len(addresses) + log.debug("Soc %d power %d pv %d pv_external %d", + soc, power, pv, pv_external) + counter_import, counter_export = sim_count(power, prefix="speicher") + get_bat_value_store(1).set(BatState(power=power, soc=soc, imported=counter_import, exported=counter_export)) + # pv_other sagt aus, ob WR definiert ist, und dessen PV Leistung auch gilt + # wenn 0 gilt nur PV und pv_external aus e3dc + pv_total = pv + pv_external + # Wenn wr1 nicht definiert ist, gilt nur die PV Leistung die hier im Modul ermittelt wurde + # als gesamte PV Leistung für wr1 + # Wenn wr1 definiert ist, gilt die bestehende PV Leistung aus Wr1 und das was hier im Modul ermittelt wurde + # als gesamte PV Leistung für wr1 + if pv_other: + pv_total = pv_total + files.pv[0].power.read() + log.debug("wr update pv_other %s pv_total %d", pv_other, pv_total) + _, exported_pv = sim_count(pv_total, prefix="pv") + get_inverter_value_store(1).set(InverterState(exported=exported_pv, power=pv_total)) + + +def main(argv: List[str]): + run_using_positional_cli_args( + {"batv19": read_legacy_batv19, "counter": read_legacy_counter}, argv + ) + + +device_descriptor = DeviceDescriptor(configuration_factory=E3dc) diff --git a/packages/modules/e3dc/inverter.py b/packages/modules/e3dc/inverter.py new file mode 100644 index 000000000..d2c1da577 --- /dev/null +++ b/packages/modules/e3dc/inverter.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +from typing import Dict, Union +import logging + +from dataclass_utils import dataclass_from_dict +from modules.common import modbus +from modules.common.component_state import InverterState +from modules.common.component_type import ComponentDescriptor +from modules.common.modbus import ModbusDataType, Endian +from modules.common.fault_state import ComponentInfo +from modules.common.store import get_inverter_value_store +from modules.common.simcount._simcounter import SimCounter +from modules.e3dc.config import E3dcInverterSetup + + +log = logging.getLogger(__name__) + + +def read_inverter(client: modbus.ModbusTcpClient_, read_ext) -> [int, int]: + # 40067 PV Leistung + pv = client.read_holding_registers(40067, ModbusDataType.INT_32, + wordorder=Endian.Little, unit=1) * -1 + if read_ext == 1: + # 40075 externe PV Leistung + pv_external = client.read_holding_registers(40075, ModbusDataType.INT_32, + wordorder=Endian.Little, unit=1) + else: + pv_external = 0 + return pv, pv_external + + +class E3dcInverter: + def __init__(self, + device_id: int, + address: str, + read_ext: int, + pvmodul: str, + component_config: Union[Dict, E3dcInverterSetup]) -> None: + self.__device_id = device_id + self.component_config = dataclass_from_dict(E3dcInverterSetup, component_config) + self.__sim_counterpv = SimCounter(self.__device_id, self.component_config.id, prefix="pv") + self.__storepv = get_inverter_value_store(self.component_config.id) + self.component_info = ComponentInfo.from_component_config(self.component_config) + self.__read_ext = read_ext + self.__address = address + self.__pvmodul = pvmodul + self.__pvother = pvmodul != "none" + + def update(self, client: modbus.ModbusTcpClient_) -> None: + + pv, pv_external = read_inverter(client, self.__read_ext) + # pv_external - > pv Leistung + # die als externe Produktion an e3dc angeschlossen ist + # nur auslesen wenn als relevant parametrisiert + # (read_external = 1) , sonst doppelte Auslesung + # pv -> pv Leistung die direkt an e3dc angeschlossen ist + log.debug("Ip: %s, read_ext %d pv_other %s", self.__address, + self.__read_ext, self.__pvother) + log.debug("pv %d pv_external %d", + pv, pv_external) + # pv_other sagt aus, ob WR definiert ist, + # und dessen PV Leistung auch gilt + # wenn 0 gilt nur PV und pv_external aus e3dc + pv_total = pv + pv_external + # Wenn wr1 nicht definiert ist, + # gilt nur die PV Leistung die hier im Modul ermittelt wurde + # als gesamte PV Leistung für wr1 + # Im gegensatz zu v1.9 implementierung wird nicht mehr die PV + # leistung vom WR1 gelesen, da die durch v2.0 separat gehandelt wird + log.debug("wr update pv_other %s pv_total %d", + self.__pvother, pv_total) + _, exportedpv = self.__sim_counterpv.sim_count(pv_total) + inverter_state = InverterState( + power=pv_total, + exported=exportedpv + ) + self.__storepv.set(inverter_state) + + +component_descriptor = ComponentDescriptor(configuration_factory=E3dcInverterSetup)