diff --git a/docs/samples/sample_modbus/sample_modbus/bat.py b/docs/samples/sample_modbus/sample_modbus/bat.py index 2b1a20a768..33dfbe574e 100644 --- a/docs/samples/sample_modbus/sample_modbus/bat.py +++ b/docs/samples/sample_modbus/sample_modbus/bat.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +from enum import IntEnum from typing import Optional, TypedDict, Any from modules.common.abstract_device import AbstractBat from modules.common.component_state import BatState @@ -15,7 +16,23 @@ class KwargsDict(TypedDict): client: ModbusTcpClient_ +class Register(IntEnum): + CURRENT_L1 = 0x06 + POWER = 0x0C + SOC = 0x46 + IMPORTED = 0x48 + EXPORTED = 0x4A + + class SampleBat(AbstractBat): + REG_MAPPING = ( + (Register.CURRENT_L1, [ModbusDataType.FLOAT_32]*3), + (Register.POWER, [ModbusDataType.FLOAT_32]*3), + (Register.SOC, ModbusDataType.FLOAT_32), + (Register.IMPORTED, ModbusDataType.FLOAT_32), + (Register.EXPORTED, ModbusDataType.FLOAT_32), + ) + def __init__(self, component_config: SampleBatSetup, **kwargs: Any) -> None: self.component_config = component_config self.kwargs: KwargsDict = kwargs @@ -29,6 +46,20 @@ def initialize(self) -> None: def update(self) -> None: unit = self.component_config.configuration.modbus_id + # Modbus-Bulk reader, liest einen Block von Registern und gibt ein Dictionary mit den Werten zurück + # read_input_registers_bulk benötigit als Parameter das Startregister, die Anzahl der Register, + # Register-Mapping und die Modbus-ID + resp = self.client.read_input_registers_bulk( + Register.CURRENT_L1, 70, mapping=self.REG_MAPPING, unit=self.id) + bat_state = BatState( + power=resp[Register.POWER], + soc=resp[Register.SOC], + imported=resp[Register.IMPORTED], + exported=resp[Register.EXPORTED], + ) + self.store.set(bat_state) + + # Einzelregister lesen (dauert länger, bei sehr weit >100 auseinanderliegenden Registern sinnvoll) power = self.client.read_holding_registers(reg, ModbusDataType.INT_32, unit=unit) soc = self.client.read_holding_registers(reg, ModbusDataType.INT_32, unit=unit) imported, exported = self.sim_counter.sim_count(power) diff --git a/docs/samples/sample_modbus/sample_modbus/counter.py b/docs/samples/sample_modbus/sample_modbus/counter.py index 5964ca63f0..8ff6d6c516 100644 --- a/docs/samples/sample_modbus/sample_modbus/counter.py +++ b/docs/samples/sample_modbus/sample_modbus/counter.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +from enum import IntEnum from typing import TypedDict, Any from modules.common.abstract_device import AbstractCounter from modules.common.component_state import CounterState @@ -15,7 +16,27 @@ class KwargsDict(TypedDict): client: ModbusTcpClient_ +class Register(IntEnum): + VOLTAGE_L1 = 0x00 + CURRENT_L1 = 0x06 + POWER_L1 = 0x0C + POWER_FACTOR_L1 = 0x1E + FREQUENCY = 0x46 + IMPORTED = 0x48 + EXPORTED = 0x4A + + class SampleCounter(AbstractCounter): + REG_MAPPING = ( + (Register.VOLTAGE_L1, [ModbusDataType.FLOAT_32]*3), + (Register.CURRENT_L1, [ModbusDataType.FLOAT_32]*3), + (Register.POWER_L1, [ModbusDataType.FLOAT_32]*3), + (Register.POWER_FACTOR_L1, [ModbusDataType.FLOAT_32]*3), + (Register.FREQUENCY, ModbusDataType.FLOAT_32), + (Register.IMPORTED, ModbusDataType.FLOAT_32), + (Register.EXPORTED, ModbusDataType.FLOAT_32), + ) + def __init__(self, component_config: SampleCounterSetup, **kwargs: Any) -> None: self.component_config = component_config self.kwargs: KwargsDict = kwargs @@ -29,6 +50,24 @@ def initialize(self) -> None: def update(self): unit = self.component_config.configuration.modbus_id + # Modbus-Bulk reader, liest einen Block von Registern und gibt ein Dictionary mit den Werten zurück + # read_input_registers_bulk benötigit als Parameter das Startregister, die Anzahl der Register, + # Register-Mapping und die Modbus-ID + resp = self.client.read_input_registers_bulk( + Register.VOLTAGE_L1, 76, mapping=self.REG_MAPPING, unit=self.id) + counter_state = CounterState( + imported=resp[Register.IMPORTED], + exported=resp[Register.EXPORTED], + power=sum(resp[Register.POWER_L1]), + voltages=resp[Register.VOLTAGE_L1], + currents=resp[Register.CURRENT_L1], + powers=resp[Register.POWER_L1], + power_factors=resp[Register.POWER_FACTOR_L1], + frequency=resp[Register.FREQUENCY], + ) + self.store.set(counter_state) + + # Einzelregister lesen (dauert länger, bei sehr weit >100 auseinanderliegenden Registern sinnvoll) power = self.client.read_holding_registers(reg, ModbusDataType.INT_32, unit=unit) imported, exported = self.sim_counter.sim_count(power) diff --git a/docs/samples/sample_modbus/sample_modbus/inverter.py b/docs/samples/sample_modbus/sample_modbus/inverter.py index c37d82bf26..cd4cdae552 100644 --- a/docs/samples/sample_modbus/sample_modbus/inverter.py +++ b/docs/samples/sample_modbus/sample_modbus/inverter.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +from enum import IntEnum from typing import TypedDict, Any from modules.common.abstract_device import AbstractInverter from modules.common.component_state import InverterState @@ -15,7 +16,21 @@ class KwargsDict(TypedDict): client: ModbusTcpClient_ +class Register(IntEnum): + CURRENT_L1 = 0x06 + POWER = 0x0C + DC_POWER = 0x48 + EXPORTED = 0x4A + + class SampleInverter(AbstractInverter): + REG_MAPPING = ( + (Register.CURRENT_L1, [ModbusDataType.FLOAT_32]*3), + (Register.POWER, [ModbusDataType.FLOAT_32]*3), + (Register.DC_POWER, ModbusDataType.FLOAT_32), + (Register.EXPORTED, ModbusDataType.FLOAT_32), + ) + def __init__(self, component_config: SampleInverterSetup, **kwargs: Any) -> None: self.component_config = component_config self.kwargs: KwargsDict = kwargs @@ -29,6 +44,20 @@ def initialize(self) -> None: def update(self) -> None: unit = self.component_config.configuration.modbus_id + # Modbus-Bulk reader, liest einen Block von Registern und gibt ein Dictionary mit den Werten zurück + # read_input_registers_bulk benötigit als Parameter das Startregister, die Anzahl der Register, + # Register-Mapping und die Modbus-ID + resp = self.client.read_input_registers_bulk( + Register.CURRENT_L1, 70, mapping=self.REG_MAPPING, unit=self.id) + inverter_state = InverterState( + power=resp[Register.POWER], + currents=resp[Register.CURRENT_L1], + dc_power=resp[Register.DC_POWER], + exported=resp[Register.EXPORTED], + ) + self.store.set(inverter_state) + + # Einzelregister lesen (dauert länger, bei sehr weit >100 auseinanderliegenden Registern sinnvoll) power = self.client.read_holding_registers(reg, ModbusDataType.INT_32, unit=unit) exported = self.sim_counter.sim_count(power)[1] diff --git a/packages/modules/common/modbus.py b/packages/modules/common/modbus.py index 49fbaa4ac9..3ef4bdd7bb 100644 --- a/packages/modules/common/modbus.py +++ b/packages/modules/common/modbus.py @@ -192,6 +192,77 @@ def write_registers(self, address: int, value: Any, **kwargs): def write_single_coil(self, address: int, value: Any, **kwargs): self._delegate.write_coil(address, value, **kwargs) + def __read_bulk(self, + read_register_method: Callable, + start_address: int, + count: int, + mapping: list[tuple[int, Union[ModbusDataType, Iterable[ModbusDataType]]]], + byteorder: Endian = Endian.Big, wordorder: Endian = Endian.Big, **kwargs): + """ + Liest einen Registerbereich und gibt ein dict mit reg als Key und dekodiertem Wert als Value zurück. + mapping: Liste von Tupeln (reg, ModbusDataType) + """ + if self.is_socket_open() is False: + self.connect() + try: + response = read_register_method(start_address, count, **kwargs) + if response.isError(): + raise Exception(__name__+" "+str(response)) + decoder = BinaryPayloadDecoder.fromRegisters(response.registers, byteorder, wordorder) + results = {} + for register_address, data_type in mapping: + multiple_register_requested = isinstance(data_type, Iterable) + if not multiple_register_requested: + data_type = [data_type] + offset = register_address - start_address + decoder.reset() + decoder.skip_bytes(offset * 2) + val = [struct.unpack(">e", struct.pack(">H", decoder.decode_16bit_uint())) if t == + ModbusDataType.FLOAT_16 else getattr(decoder, t.decoding_method)() for t in data_type] + results[register_address] = val if multiple_register_requested else val[0] + return results + except pymodbus.exceptions.ConnectionException as e: + self.close() + e.args += (NO_CONNECTION.format(self.address, self.port),) + raise e + except pymodbus.exceptions.ModbusIOException as e: + self.close() + e.args += (NO_VALUES.format(self.address, self.port),) + raise e + except Exception as e: + self.close() + raise Exception(__name__+" "+str(type(e))+" " + str(e)) from e + + def read_input_registers_bulk(self, + start_address: int, + count: int, + mapping: list[tuple[int, Union[ModbusDataType, Iterable[ModbusDataType]]]], + byteorder: Endian = Endian.Big, + wordorder: Endian = Endian.Big, + **kwargs): + return self.__read_bulk(self._delegate.read_input_registers, + start_address, + count, + mapping, + byteorder, + wordorder, + **kwargs) + + def read_holding_registers_bulk(self, + start_address: int, + count: int, + mapping: list[tuple[int, Union[ModbusDataType, Iterable[ModbusDataType]]]], + byteorder: Endian = Endian.Big, + wordorder: Endian = Endian.Big, + **kwargs): + return self.__read_bulk(self._delegate.read_holding_registers, + start_address, + count, + mapping, + byteorder, + wordorder, + **kwargs) + class ModbusTcpClient_(ModbusClient): def __init__(self, diff --git a/packages/modules/common/sdm.py b/packages/modules/common/sdm.py index b77eec9eac..84a00e3159 100644 --- a/packages/modules/common/sdm.py +++ b/packages/modules/common/sdm.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -import time -from typing import List, Tuple +from enum import IntEnum from modules.common import modbus from modules.common.abstract_counter import AbstractCounter @@ -14,76 +13,50 @@ class Sdm(AbstractCounter): def __init__(self, modbus_id: int, client: modbus.ModbusTcpClient_) -> None: self.client = client self.id = modbus_id - self.last_query = self._get_time_ms() - self.WAIT_MS_BETWEEN_QUERIES = 100 with client: self.serial_number = str(self.client.read_holding_registers(0xFC00, ModbusDataType.UINT_32, unit=self.id)) - def get_imported(self) -> float: - self._ensure_min_time_between_queries() - return self.client.read_input_registers(0x0048, ModbusDataType.FLOAT_32, unit=self.id) * 1000 - def get_exported(self) -> float: - self._ensure_min_time_between_queries() - return self.client.read_input_registers(0x004a, ModbusDataType.FLOAT_32, unit=self.id) * 1000 - - def get_frequency(self) -> float: - self._ensure_min_time_between_queries() - frequency = self.client.read_input_registers(0x46, ModbusDataType.FLOAT_32, unit=self.id) - if frequency > 100: - frequency = frequency / 10 - return frequency - - # These meters require some minimum time between subsequent Modbus reads. Some Eastron papers recommend 100 ms. - # Sometimes the time between calls to the get_* methods are much shorter so we forcibly wait for the remaining time. - def _ensure_min_time_between_queries(self) -> None: - current_time = self._get_time_ms() - elapsed_time = current_time - self.last_query - if elapsed_time < self.WAIT_MS_BETWEEN_QUERIES: - time.sleep((self.WAIT_MS_BETWEEN_QUERIES - elapsed_time) / 1e3) - self.last_query = current_time - - def _get_time_ms(self) -> float: - return time.time_ns() / 1e6 - - def get_serial_number(self) -> str: - return self.serial_number +class SdmRegister(IntEnum): + VOLTAGE_L1 = 0x00 + CURRENT_L1 = 0x06 + POWER_L1 = 0x0C + POWER_FACTOR_L1 = 0x1E + FREQUENCY = 0x46 + IMPORTED = 0x48 + EXPORTED = 0x4A class Sdm630_72(Sdm): + REG_MAPPING = ( + (SdmRegister.VOLTAGE_L1, [ModbusDataType.FLOAT_32]*3), + (SdmRegister.CURRENT_L1, [ModbusDataType.FLOAT_32]*3), + (SdmRegister.POWER_L1, [ModbusDataType.FLOAT_32]*3), + (SdmRegister.POWER_FACTOR_L1, [ModbusDataType.FLOAT_32]*3), + (SdmRegister.FREQUENCY, ModbusDataType.FLOAT_32), + (SdmRegister.IMPORTED, ModbusDataType.FLOAT_32), + (SdmRegister.EXPORTED, ModbusDataType.FLOAT_32), + ) + def __init__(self, modbus_id: int, client: modbus.ModbusTcpClient_, fault_state: FaultState) -> None: super().__init__(modbus_id, client) self.fault_state = fault_state - def get_currents(self) -> List[float]: - self._ensure_min_time_between_queries() - return self.client.read_input_registers(0x06, [ModbusDataType.FLOAT_32]*3, unit=self.id) - - def get_power_factors(self) -> List[float]: - self._ensure_min_time_between_queries() - return self.client.read_input_registers(0x1E, [ModbusDataType.FLOAT_32]*3, unit=self.id) - - def get_power(self) -> Tuple[List[float], float]: - self._ensure_min_time_between_queries() - powers = self.client.read_input_registers(0x0C, [ModbusDataType.FLOAT_32]*3, unit=self.id) - power = sum(powers) - return powers, power - - def get_voltages(self) -> List[float]: - self._ensure_min_time_between_queries() - return self.client.read_input_registers(0x00, [ModbusDataType.FLOAT_32]*3, unit=self.id) - def get_counter_state(self) -> CounterState: - powers, power = self.get_power() + resp = self.client.read_input_registers_bulk( + SdmRegister.VOLTAGE_L1, 76, mapping=self.REG_MAPPING, unit=self.id) + frequency = resp[SdmRegister.FREQUENCY] + if frequency > 100: + frequency = frequency / 10 counter_state = CounterState( - imported=self.get_imported(), - exported=self.get_exported(), - power=power, - voltages=self.get_voltages(), - currents=self.get_currents(), - powers=powers, - power_factors=self.get_power_factors(), - frequency=self.get_frequency(), + imported=resp[SdmRegister.IMPORTED]*1000, + exported=resp[SdmRegister.EXPORTED]*1000, + power=sum(resp[SdmRegister.POWER_L1]), + voltages=resp[SdmRegister.VOLTAGE_L1], + currents=resp[SdmRegister.CURRENT_L1], + powers=resp[SdmRegister.POWER_L1], + power_factors=resp[SdmRegister.POWER_FACTOR_L1], + frequency=frequency, serial_number=self.get_serial_number() ) check_meter_values(counter_state, self.fault_state) @@ -91,37 +64,35 @@ def get_counter_state(self) -> CounterState: class Sdm120(Sdm): + REG_MAPPING = ( + (SdmRegister.VOLTAGE_L1, ModbusDataType.FLOAT_32), + (SdmRegister.CURRENT_L1, ModbusDataType.FLOAT_32), + (SdmRegister.POWER_L1, ModbusDataType.FLOAT_32), + (SdmRegister.POWER_FACTOR_L1, ModbusDataType.FLOAT_32), + (SdmRegister.FREQUENCY, ModbusDataType.FLOAT_32), + (SdmRegister.IMPORTED, ModbusDataType.FLOAT_32), + (SdmRegister.EXPORTED, ModbusDataType.FLOAT_32), + ) + def __init__(self, modbus_id: int, client: modbus.ModbusTcpClient_, fault_state: FaultState) -> None: super().__init__(modbus_id, client) self.fault_state = fault_state - def get_power(self) -> Tuple[List[float], float]: - self._ensure_min_time_between_queries() - power = self.client.read_input_registers(0x0C, ModbusDataType.FLOAT_32, unit=self.id) - return [power, 0, 0], power - - def get_currents(self) -> List[float]: - self._ensure_min_time_between_queries() - return [self.client.read_input_registers(0x06, ModbusDataType.FLOAT_32, unit=self.id), 0.0, 0.0] - - def get_voltages(self) -> List[float]: - self._ensure_min_time_between_queries() - voltage = self.client.read_input_registers(0x00, ModbusDataType.FLOAT_32, unit=self.id) - return [voltage, 0.0, 0.0] - - def get_power_factors(self) -> List[float]: - self._ensure_min_time_between_queries() - return [self.client.read_input_registers(0x1E, ModbusDataType.FLOAT_32, unit=self.id), 0.0, 0.0] - def get_counter_state(self) -> CounterState: - powers, power = self.get_power() + resp = self.client.read_input_registers_bulk( + SdmRegister.VOLTAGE_L1, 76, mapping=self.REG_MAPPING, unit=self.id) + frequency = resp[SdmRegister.FREQUENCY] + if frequency > 100: + frequency = frequency / 10 counter_state = CounterState( - imported=self.get_imported(), - exported=self.get_exported(), - power=power, - currents=self.get_currents(), - powers=powers, - frequency=self.get_frequency(), + imported=resp[SdmRegister.IMPORTED]*1000, + exported=resp[SdmRegister.EXPORTED]*1000, + power=resp[SdmRegister.POWER_L1], + voltages=[resp[SdmRegister.VOLTAGE_L1], 0, 0], + currents=[resp[SdmRegister.CURRENT_L1], 0, 0], + powers=[resp[SdmRegister.POWER_L1], 0, 0], + power_factors=[resp[SdmRegister.POWER_FACTOR_L1], 0, 0], + frequency=frequency, serial_number=self.get_serial_number() ) check_meter_values(counter_state, self.fault_state) diff --git a/packages/modules/devices/solaredge/solaredge/counter.py b/packages/modules/devices/solaredge/solaredge/counter.py index 03ea24a0a6..21f9073bd0 100644 --- a/packages/modules/devices/solaredge/solaredge/counter.py +++ b/packages/modules/devices/solaredge/solaredge/counter.py @@ -10,7 +10,7 @@ from modules.common.modbus import ModbusDataType from modules.common.store import get_counter_value_store from modules.devices.solaredge.solaredge.config import SolaredgeCounterSetup -from modules.devices.solaredge.solaredge.scale import create_scaled_reader +from modules.devices.solaredge.solaredge.scale import scale_registers from modules.devices.solaredge.solaredge.meter import SolaredgeMeterRegisters, set_component_registers log = logging.getLogger(__name__) @@ -36,31 +36,36 @@ def initialize(self) -> None: components.append(self) set_component_registers(self.component_config, self.__tcp_client, components) - self._read_scaled_int16 = create_scaled_reader( - self.__tcp_client, self.component_config.configuration.modbus_id, ModbusDataType.INT_16 - ) - self._read_scaled_uint32 = create_scaled_reader( - self.__tcp_client, self.component_config.configuration.modbus_id, ModbusDataType.UINT_32 + def update(self): + reg_mapping = ( + (self.registers.voltages, [ModbusDataType.INT_16]*3), + (self.registers.voltages_scale, ModbusDataType.INT_16), + (self.registers.currents, [ModbusDataType.INT_16]*3), + (self.registers.currents_scale, ModbusDataType.INT_16), + (self.registers.powers, [ModbusDataType.INT_16]*3), + (self.registers.power, ModbusDataType.INT_16), + (self.registers.powers_scale, ModbusDataType.INT_16), + (self.registers.power_factors, [ModbusDataType.INT_16]*3), + (self.registers.power_factors_scale, ModbusDataType.INT_16), + (self.registers.frequency, ModbusDataType.INT_16), + (self.registers.imported, ModbusDataType.UINT_32), + (self.registers.exported, ModbusDataType.UINT_32), + (self.registers.imp_exp_scale, ModbusDataType.INT_16), ) + resp = self.__tcp_client.read_holding_registers_bulk( + self.registers.currents, 52, mapping=reg_mapping, unit=self.component_config.configuration.modbus_id) - def update(self): - powers = [-power for power in self._read_scaled_int16(self.registers.powers, 4)] - currents = self._read_scaled_int16(self.registers.currents, 3) - voltages = self._read_scaled_int16(self.registers.voltages, 7)[:3] - frequency = self._read_scaled_int16(self.registers.frequency, 1)[0] - power_factors = [power_factor / - 100 for power_factor in self._read_scaled_int16(self.registers.power_factors, 3)] - counter_values = self._read_scaled_uint32(self.registers.imp_exp, 8) - counter_exported, counter_imported = [counter_values[i] for i in [0, 4]] counter_state = CounterState( - imported=counter_imported, - exported=counter_exported, - power=powers[0], - powers=powers[1:], - voltages=voltages, - currents=currents, - power_factors=power_factors, - frequency=frequency + imported=scale_registers(resp[self.registers.imported], resp[self.registers.imp_exp_scale]), + exported=scale_registers(resp[self.registers.exported], resp[self.registers.imp_exp_scale]), + power=scale_registers(resp[self.registers.power], resp[self.registers.powers_scale]) * -1, + powers=[-power for power in scale_registers(resp[self.registers.powers], + resp[self.registers.powers_scale])], + voltages=scale_registers(resp[self.registers.voltages], resp[self.registers.voltages_scale]), + currents=scale_registers(resp[self.registers.currents], resp[self.registers.currents_scale]), + power_factors=[power_factor / 100 for power_factor in scale_registers( + resp[self.registers.power_factors], resp[self.registers.power_factors_scale])], + frequency=scale_registers(resp[self.registers.frequency], resp[self.registers.frequency_scale]), ) self.store.set(counter_state) diff --git a/packages/modules/devices/solaredge/solaredge/external_inverter.py b/packages/modules/devices/solaredge/solaredge/external_inverter.py index e4f4185d62..5a6599073a 100644 --- a/packages/modules/devices/solaredge/solaredge/external_inverter.py +++ b/packages/modules/devices/solaredge/solaredge/external_inverter.py @@ -10,7 +10,7 @@ from modules.common.modbus import ModbusDataType from modules.common.store import get_inverter_value_store from modules.devices.solaredge.solaredge.config import SolaredgeExternalInverterSetup -from modules.devices.solaredge.solaredge.scale import create_scaled_reader +from modules.devices.solaredge.solaredge.scale import scale_registers from modules.devices.solaredge.solaredge.meter import SolaredgeMeterRegisters, set_component_registers log = logging.getLogger(__name__) @@ -38,26 +38,26 @@ def initialize(self) -> None: components.append(self) set_component_registers(self.component_config, self.__tcp_client, components) - self._read_scaled_int16 = create_scaled_reader( - self.__tcp_client, self.component_config.configuration.modbus_id, ModbusDataType.INT_16 - ) - self._read_scaled_uint32 = create_scaled_reader( - self.__tcp_client, self.component_config.configuration.modbus_id, ModbusDataType.UINT_32 - ) - def update(self) -> None: self.store.set(self.read_state()) def read_state(self) -> InverterState: - factor = self.component_config.configuration.factor - power = self._read_scaled_int16(self.registers.powers, 4)[0] * factor - exported = self._read_scaled_uint32(self.registers.imp_exp, 8)[0] - currents = self._read_scaled_int16(self.registers.currents, 3) + reg_mapping = ( + (self.registers.currents, [ModbusDataType.INT_16]*3), + (self.registers.currents_scale, ModbusDataType.INT_16), + (self.registers.power, ModbusDataType.INT_16), + (self.registers.powers_scale, ModbusDataType.INT_16), + (self.registers.exported, ModbusDataType.UINT_32), + (self.registers.imp_exp_scale, ModbusDataType.INT_16), + ) + resp = self.__tcp_client.read_holding_registers_bulk( + self.registers.currents, 51, mapping=reg_mapping, unit=self.component_config.configuration.modbus_id) + factor = self.component_config.configuration.factor return InverterState( - exported=exported, - power=power, - currents=currents + exported=scale_registers(resp[self.registers.exported], resp[self.registers.imp_exp_scale]), + power=scale_registers(resp[self.registers.power], resp[self.registers.powers_scale]) * factor, + currents=scale_registers(resp[self.registers.currents], resp[self.registers.currents_scale]) ) diff --git a/packages/modules/devices/solaredge/solaredge/inverter.py b/packages/modules/devices/solaredge/solaredge/inverter.py index 708e45ed4c..9e40716dfa 100644 --- a/packages/modules/devices/solaredge/solaredge/inverter.py +++ b/packages/modules/devices/solaredge/solaredge/inverter.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +from enum import IntEnum from typing import TypedDict, Any from modules.common import modbus @@ -9,7 +10,7 @@ from modules.common.modbus import ModbusDataType from modules.common.store import get_inverter_value_store from modules.devices.solaredge.solaredge.config import SolaredgeInverterSetup -from modules.devices.solaredge.solaredge.scale import create_scaled_reader +from modules.devices.solaredge.solaredge.scale import scale_registers from modules.common.simcount import SimCounter @@ -18,7 +19,29 @@ class KwargsDict(TypedDict): device_id: int +class Register(IntEnum): + POWER = 40083 + POWER_SCALE = 40084 + EXPORTED = 40093 + EXPORTED_SCALE = 40095 + CURRENTS = 40072 + CURRENTS_SCALE = 40075 + DC_POWER = 40100 + DC_POWER_SCALE = 40101 + + class SolaredgeInverter(AbstractInverter): + REG_MAPPING = ( + (Register.POWER, ModbusDataType.INT_16), + (Register.POWER_SCALE, ModbusDataType.INT_16), + (Register.EXPORTED, ModbusDataType.UINT_32), + (Register.EXPORTED_SCALE, ModbusDataType.INT_16), + (Register.CURRENTS, [ModbusDataType.UINT_16]*3), + (Register.CURRENTS_SCALE, ModbusDataType.INT_16), + (Register.DC_POWER, ModbusDataType.INT_16), + (Register.DC_POWER_SCALE, ModbusDataType.INT_16), + ) + def __init__(self, component_config: SolaredgeInverterSetup, **kwargs: Any) -> None: @@ -29,43 +52,23 @@ def initialize(self) -> None: self.__tcp_client = self.kwargs['client'] self.store = get_inverter_value_store(self.component_config.id) self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config)) - self._read_scaled_int16 = create_scaled_reader( - self.__tcp_client, self.component_config.configuration.modbus_id, ModbusDataType.INT_16 - ) - self._read_scaled_uint16 = create_scaled_reader( - self.__tcp_client, self.component_config.configuration.modbus_id, ModbusDataType.UINT_16 - ) - self._read_scaled_uint32 = create_scaled_reader( - self.__tcp_client, self.component_config.configuration.modbus_id, ModbusDataType.UINT_32 - ) self.sim_counter = SimCounter(self.kwargs['device_id'], self.component_config.id, prefix="Wechselrichter") def update(self) -> None: self.store.set(self.read_state()) def read_state(self): - # 40083 = AC Power value (Watt) - # 40084 = AC Power scale factor - power = self._read_scaled_int16(40083, 1)[0] * -1 - - # 40093 = AC Lifetime Energy production (Watt hours) - # 40095 = AC Lifetime scale factor - exported = self._read_scaled_uint32(40093, 1)[0] - # 40072/40073/40074 = AC Phase A/B/C Current value (Amps) - # 40075 = AC Current scale factor - currents = self._read_scaled_uint16(40072, 3) - # 40100 = DC Power value (Watt) - # 40101 = DC Power scale factor - # Wenn bei Hybrid-Systemen der Speicher aus dem Netz geladen wird, ist die DC-Leistung negativ. - dc_power = self._read_scaled_int16(40100, 1)[0] * -1 + resp = self.__tcp_client.read_holding_registers_bulk( + Register.POWER, 18, mapping=self.REG_MAPPING, unit=self.component_config.configuration.modbus_id) + power = scale_registers(resp[Register.POWER], resp[Register.POWER_SCALE]) * -1 imported, _ = self.sim_counter.sim_count(power) return InverterState( power=power, - exported=exported, - currents=currents, - dc_power=dc_power, + exported=scale_registers(resp[Register.EXPORTED], resp[Register.EXPORTED_SCALE]), + currents=scale_registers(resp[Register.CURRENTS], resp[Register.CURRENTS_SCALE]), + dc_power=scale_registers(resp[Register.DC_POWER], resp[Register.DC_POWER_SCALE]) * -1, imported=imported, ) diff --git a/packages/modules/devices/solaredge/solaredge/inverter_test.py b/packages/modules/devices/solaredge/solaredge/inverter_test.py index 496ad865ae..b1f4f83cf7 100644 --- a/packages/modules/devices/solaredge/solaredge/inverter_test.py +++ b/packages/modules/devices/solaredge/solaredge/inverter_test.py @@ -9,14 +9,14 @@ def test_read_state(): # setup - mock_read_holding_registers = Mock(side_effect=[ - [14152, -1], - [8980404, 0], - [616, 65535, 65535, -2], - [14368, -1] - ]) + mock_read_holding_registers_bulk = Mock(side_effect=[{ + 40083: 14152, 40084: -1, + 40093: 8980404, 40095: 0, + 40072: [616, 65535, 65535], 40075: -2, + 40100: 14368, 40101: -1, + }]) inverter = SolaredgeInverter(SolaredgeInverterSetup(), client=Mock( - spec=ModbusTcpClient_, read_holding_registers=mock_read_holding_registers), device_id=1) + spec=ModbusTcpClient_, read_holding_registers_bulk=mock_read_holding_registers_bulk), device_id=1) inverter.initialize() # execution diff --git a/packages/modules/devices/solaredge/solaredge/meter.py b/packages/modules/devices/solaredge/solaredge/meter.py index ebf7d2c257..cd127ddce0 100644 --- a/packages/modules/devices/solaredge/solaredge/meter.py +++ b/packages/modules/devices/solaredge/solaredge/meter.py @@ -2,7 +2,7 @@ import logging from typing import Iterable, Union, List -from modules.common import modbus +from modules.common.modbus import ModbusDataType from modules.devices.solaredge.solaredge.config import (SolaredgeBatSetup, SolaredgeCounterSetup, SolaredgeExternalInverterSetup, SolaredgeInverterSetup) log = logging.getLogger(__name__) @@ -16,25 +16,33 @@ def __init__(self, internal_meter_id: int = 1, synergy_units: int = 1): # 40206: Total Real Power (sum of active phases) # 40207/40208/40209: Real Power by phase # 40210: AC Real Power Scale Factor - self.powers = 40206 + self.power = 40206 + self.powers = 40207 + self.powers_scale = 40210 # 40191/40192/40193: AC Current by phase # 40194: AC Current Scale Factor self.currents = 40191 + self.currents_scale = 40194 # 40196/40197/40198: Voltage per phase # 40203: AC Voltage Scale Factor self.voltages = 40196 + self.voltages_scale = 40203 # 40204: AC Frequency # 40205: AC Frequency Scale Factor self.frequency = 40204 + self.frequency_scale = 40205 # 40222/40223/40224: Power factor by phase (unit=%) # 40225: AC Power Factor Scale Factor self.power_factors = 40222 + self.power_factors_scale = 40225 # 40226: Total Exported Real Energy # 40228/40230/40232: Total Exported Real Energy Phase (not used) # 40234: Total Imported Real Energy # 40236/40238/40240: Total Imported Real Energy Phase (not used) # 40242: Real Energy Scale Factor - self.imp_exp = 40226 + self.exported = 40226 + self.imported = 40234 + self.imp_exp_scale = 40242 # 40155: C_Option Export + Import, Production, consumption, self.option = 40155 self._update_offset_meter_id(internal_meter_id) @@ -87,14 +95,14 @@ def _get_synergy_units(component_config: Union[SolaredgeBatSetup, SolaredgeInverterSetup, SolaredgeExternalInverterSetup], client) -> int: - if client.read_holding_registers(40121, modbus.ModbusDataType.UINT_16, + if client.read_holding_registers(40121, ModbusDataType.UINT_16, unit=component_config.configuration.modbus_id ) == synergy_unit_identifier: # Snyergy-Units vom Haupt-WR des angeschlossenen Meters ermitteln. Es kann mehrere Haupt-WR mit # unterschiedlichen Modbus-IDs im Verbund geben. log.debug("Synergy Units supported") synergy_units = int(client.read_holding_registers( - 40129, modbus.ModbusDataType.UINT_16, + 40129, ModbusDataType.UINT_16, unit=component_config.configuration.modbus_id)) or 1 log.debug( f"Synergy Units detected for Modbus ID {component_config.configuration.modbus_id}: {synergy_units}") diff --git a/packages/modules/devices/solaredge/solaredge/meter_test.py b/packages/modules/devices/solaredge/solaredge/meter_test.py index 02223c9d26..4aa8c15836 100644 --- a/packages/modules/devices/solaredge/solaredge/meter_test.py +++ b/packages/modules/devices/solaredge/solaredge/meter_test.py @@ -26,7 +26,7 @@ def test_meter(params: Params): registers = SolaredgeMeterRegisters(params.meter_id, params.synergy_units) # assert - assert registers.powers == params.expected_power_register + assert registers.power == params.expected_power_register Params = NamedTuple("Params", [("configured_meter_ids", List[int])]) @@ -61,5 +61,5 @@ def test_set_component_registers_assigns_effective_meter_regs(params: Params): _set_registers(components_list, synergy_units=1, modbus_id=1) # evaluation - assert components_list[0].registers.powers == 40206 - assert components_list[1].registers.powers == 40380 + assert components_list[0].registers.power == 40206 + assert components_list[1].registers.power == 40380 diff --git a/packages/modules/devices/solaredge/solaredge/scale.py b/packages/modules/devices/solaredge/solaredge/scale.py index bfe78d2ddd..3f45b15dc9 100644 --- a/packages/modules/devices/solaredge/solaredge/scale.py +++ b/packages/modules/devices/solaredge/solaredge/scale.py @@ -1,8 +1,8 @@ import logging import math -from typing import List +from typing import Iterable, List, Union -from modules.common.modbus import ModbusDataType, ModbusTcpClient_, Number +from modules.common.modbus import Number log = logging.getLogger(__name__) @@ -12,16 +12,9 @@ UINT16_UNSUPPORTED = 0xFFFF -def scale_registers(registers: List[Number]) -> List[float]: - log.debug("Registers %s, Scale %s", registers[:-1], registers[-1]) - scale = math.pow(10, registers[-1]) - return [register * scale if register != UINT16_UNSUPPORTED else 0 for register in registers[:-1]] - - -def create_scaled_reader(client: ModbusTcpClient_, modbus_id: int, type: ModbusDataType): - def scaled_reader(address: int, count: int): - return scale_registers( - client.read_holding_registers(address, [type] * count + [ModbusDataType.INT_16], unit=modbus_id) - ) - - return scaled_reader +def scale_registers(registers: Union[List[Number], Number], scale: float) -> List[float]: + log.debug("Registers %s, Scale %s", registers, scale) + if not isinstance(registers, Iterable): + return registers * math.pow(10, scale) if registers != UINT16_UNSUPPORTED else 0 + else: + return [register * math.pow(10, scale) if register != UINT16_UNSUPPORTED else 0 for register in registers]