Skip to content

Commit

Permalink
Solaredge: consider synergy units
Browse files Browse the repository at this point in the history
  • Loading branch information
LKuemmel committed Sep 19, 2022
1 parent 5f69090 commit 11ce0a7
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 56 deletions.
6 changes: 4 additions & 2 deletions packages/modules/solaredge/config.py
Expand Up @@ -38,8 +38,9 @@ def __init__(self,


class SolaredgeCounterConfiguration:
def __init__(self, modbus_id: int = 1):
def __init__(self, modbus_id: int = 1, meter_id: int = 1):
self.modbus_id = modbus_id
self.meter_id = meter_id


class SolaredgeCounterSetup(ComponentSetup[SolaredgeCounterConfiguration]):
Expand All @@ -52,8 +53,9 @@ def __init__(self,


class SolaredgeExternalInverterConfiguration:
def __init__(self, modbus_id: int = 1):
def __init__(self, modbus_id: int = 1, meter_id: int = 2):
self.modbus_id = modbus_id
self.meter_id = meter_id


class SolaredgeExternalInverterSetup(ComponentSetup[SolaredgeExternalInverterConfiguration]):
Expand Down
42 changes: 12 additions & 30 deletions packages/modules/solaredge/counter.py
@@ -1,4 +1,5 @@
#!/usr/bin/env python3
import logging
from typing import Dict, Union

from dataclass_utils import dataclass_from_dict
Expand All @@ -10,6 +11,9 @@
from modules.common.store import get_counter_value_store
from modules.solaredge.config import SolaredgeCounterSetup
from modules.solaredge.scale import scale_registers
from modules.solaredge.meter import SolaredgeMeterRegisters

log = logging.getLogger(__name__)


class SolaredgeCounter:
Expand All @@ -19,6 +23,7 @@ def __init__(self,
tcp_client: modbus.ModbusTcpClient_) -> None:
self.component_config = dataclass_from_dict(SolaredgeCounterSetup, component_config)
self.__tcp_client = tcp_client
self.registers = SolaredgeMeterRegisters()
self.__store = get_counter_value_store(self.component_config.id)
self.component_info = ComponentInfo.from_component_config(self.component_config)

Expand All @@ -35,39 +40,16 @@ def read_scaled_uint32(address: int, count: int):
return scale_registers(
self.__tcp_client.read_holding_registers(
address,
[ModbusDataType.UINT_32] * (count)+[ModbusDataType.INT_16],
[ModbusDataType.UINT_32] * count+[ModbusDataType.INT_16],
unit=self.component_config.configuration.modbus_id)
)

# 40206: Total Real Power (sum of active phases)
# 40207/40208/40209: Real Power by phase
# 40210: AC Real Power Scale Factor
powers = [-power for power in read_scaled_int16(40206, 4)]

# 40191/40192/40193: AC Current by phase
# 40194: AC Current Scale Factor
currents = read_scaled_int16(40191, 3)

# 40196/40197/40198: Voltage per phase
# 40203: AC Voltage Scale Factor
voltages = read_scaled_int16(40196, 7)[:3]

# 40204: AC Frequency
# 40205: AC Frequency Scale Factor
frequency = read_scaled_int16(40204, 1)[0]

# 40222/40223/40224: Power factor by phase (unit=%)
# 40225: AC Power Factor Scale Factor
power_factors = [power_factor / 100 for power_factor in read_scaled_int16(40222, 3)]

# 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
counter_values = read_scaled_uint32(40226, 8)
powers = [-power for power in read_scaled_int16(self.registers.powers, 4)]
currents = read_scaled_int16(self.registers.currents, 3)
voltages = read_scaled_int16(self.registers.voltages, 7)[:3]
frequency = read_scaled_int16(self.registers.frequency, 1)[0]
power_factors = [power_factor / 100 for power_factor in read_scaled_int16(self.registers.power_factors, 3)]
counter_values = 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,
Expand Down
52 changes: 52 additions & 0 deletions packages/modules/solaredge/devcie_test.py
@@ -0,0 +1,52 @@
from typing import Tuple
from unittest.mock import Mock

import pytest

from modules.solaredge import device
from modules.solaredge.config import (SolaredgeCounterConfiguration, SolaredgeCounterSetup,
SolaredgeExternalInverterConfiguration, SolaredgeExternalInverterSetup)
from modules.solaredge.counter import SolaredgeCounter
from modules.solaredge.external_inverter import SolaredgeExternalInverter


class Params:
def __init__(self,
name: str,
meter_id_counter: int,
meter_id_inverter: int,
synergy_units: int,
expected_counter_register: Tuple[int, int],
expected_inverter_register: Tuple[int, int]):
self.name = name
self.meter_id_counter = meter_id_counter
self.meter_id_inverter = meter_id_inverter
self.synergy_units = synergy_units
self.expected_counter_register = expected_counter_register
self.expected_inverter_register = expected_inverter_register


cases = [Params("counter id 1, inverter id 2, synergy units 1", 1, 2, 1, (1, 1), (2, 1)),
Params("counter id 1, inverter id 3, synergy units 2", 1, 3, 2, (1, 2), (2, 2)),
Params("counter id 2, inverter id 1, synergy units 3", 2, 1, 3, (2, 3), (1, 3)),
Params("counter id 3, inverter id 1, synergy units 1", 3, 1, 1, (2, 1), (1, 1))
]


@pytest.mark.parametrize("params", cases, ids=[c.name for c in cases])
def test_set_component_registers(monkeypatch, params: Params):
# setup
client = Mock()
monkeypatch.setattr(device, "SolaredgeMeterRegisters", Mock(side_effect=lambda *args: args))
counter = SolaredgeCounter(2, SolaredgeCounterSetup(
configuration=SolaredgeCounterConfiguration(meter_id=params.meter_id_counter)), client)
external_inverter = SolaredgeExternalInverter(2, SolaredgeExternalInverterSetup(
configuration=SolaredgeExternalInverterConfiguration(meter_id=params.meter_id_inverter)), client)
components = {"component1": counter, "component2": external_inverter}

# execution
device.Device.set_component_registers(components, synergy_units=params.synergy_units)

# evaluation
assert external_inverter.registers == params.expected_inverter_register
assert counter.registers == params.expected_counter_register
57 changes: 41 additions & 16 deletions packages/modules/solaredge/device.py
Expand Up @@ -18,28 +18,30 @@
from modules.common.component_state import BatState, InverterState
from modules.common.fault_state import ComponentInfo
from modules.common.store import get_inverter_value_store, get_bat_value_store
from modules.solaredge import bat
from modules.solaredge import counter
from modules.solaredge import external_inverter
from modules.solaredge import inverter
from modules.solaredge import bat, counter, external_inverter, inverter
from modules.solaredge.bat import SolaredgeBat
from modules.solaredge.counter import SolaredgeCounter
from modules.solaredge.external_inverter import SolaredgeExternalInverter
from modules.solaredge.inverter import SolaredgeInverter
from modules.solaredge.config import (Solaredge, SolaredgeBatConfiguration, SolaredgeBatSetup, SolaredgeConfiguration,
SolaredgeCounterConfiguration, SolaredgeCounterSetup,
SolaredgeExternalInverterConfiguration, SolaredgeExternalInverterSetup,
SolaredgeInverterConfiguration, SolaredgeInverterSetup)
from modules.solaredge.meter import SolaredgeMeterRegisters

log = logging.getLogger(__name__)

solaredge_component_classes = Union[bat.SolaredgeBat, counter.SolaredgeCounter,
external_inverter.SolaredgeExternalInverter, inverter.SolaredgeInverter]
solaredge_component_classes = Union[SolaredgeBat, SolaredgeCounter,
SolaredgeExternalInverter, SolaredgeInverter]
default_unit_id = 85


class Device(AbstractDevice):
COMPONENT_TYPE_TO_CLASS = {
"bat": bat.SolaredgeBat,
"counter": counter.SolaredgeCounter,
"inverter": inverter.SolaredgeInverter,
"external_inverter": external_inverter.SolaredgeExternalInverter
"bat": SolaredgeBat,
"counter": SolaredgeCounter,
"inverter": SolaredgeInverter,
"external_inverter": SolaredgeExternalInverter
}

def __init__(self, device_config: Union[Dict, Solaredge]) -> None:
Expand All @@ -49,6 +51,7 @@ def __init__(self, device_config: Union[Dict, Solaredge]) -> None:
self.client = modbus.ModbusTcpClient_(self.device_config.configuration.ip_address,
self.device_config.configuration.port)
self.inverter_counter = 0
self.synergy_units = 1
except Exception:
log.exception("Fehler im Modul "+self.device_config.name)

Expand All @@ -69,12 +72,34 @@ def add_component(self,
self.device_config.id, component_config, self.client))
if component_type == "inverter" or component_type == "external_inverter":
self.inverter_counter += 1
self.synergy_units = int(self.client.read_holding_registers(
40129, modbus.ModbusDataType.UINT_16,
unit=component_config.configuration.modbus_id)) or 1
log.debug("Synergy Units: %s", self.synergy_units)
if component_type == "external_inverter" or component_type == "counter" or component_type == "inverter":
self.set_component_registers(self.components, self.synergy_units)
else:
raise Exception(
"illegal component type " + component_type + ". Allowed values: " +
','.join(self.COMPONENT_TYPE_TO_CLASS.keys())
)

@staticmethod
def set_component_registers(components: Dict[str, solaredge_component_classes], synergy_units: int) -> None:
meters = [None]*3 # type: List[Union[SolaredgeExternalInverter, SolaredgeCounter, None]]
for component in components.values():
if isinstance(component, (SolaredgeExternalInverter, SolaredgeCounter)):
meters[component.component_config.configuration.meter_id-1] = component

# https://www.solaredge.com/sites/default/files/sunspec-implementation-technical-note.pdf:
# Only enabled meters are readable, i.e. if meter 1 and 3 are enabled, they are readable as 1st meter and 2nd
# meter (and the 3rd meter isn't readable).
for meter_id, meter in enumerate(list(filter(None, meters)), start=1):
log.debug(
"%s: internal meter id: %d, synergy units: %s", meter.component_config.name, meter_id, synergy_units
)
meter.registers = SolaredgeMeterRegisters(meter_id, synergy_units)

def update(self) -> None:
log.debug("Start device reading " + str(self.components))
if self.components:
Expand All @@ -95,15 +120,14 @@ def update(self) -> None:
else:
total_power -= state.power
for component in self.components.values():
if (isinstance(component, inverter.SolaredgeInverter) or
isinstance(component, external_inverter.SolaredgeExternalInverter)):
if isinstance(component, (SolaredgeInverter, SolaredgeExternalInverter)):
state = component.read_state()
# In 1.9 wurde bisher die Summe der WR-Leistung um die Summe der Batterie-Leistung
# bereinigt. Zähler und Ströme wurden nicht bereinigt.
state.power = state.power - total_power/self.inverter_counter
component.update(state)
for component in self.components.values():
if isinstance(component, counter.SolaredgeCounter):
if isinstance(component, SolaredgeCounter):
component.update()
else:
log.warning(
Expand All @@ -115,6 +139,7 @@ def update(self) -> None:
COMPONENT_TYPE_TO_MODULE = {
"bat": bat,
"counter": counter,
"external_inverter": external_inverter,
"inverter": inverter
}

Expand Down Expand Up @@ -153,14 +178,14 @@ def get_external_inverter_state(dev: Device, id: int) -> InverterState:
configuration=SolaredgeExternalInverterConfiguration(
modbus_id=id))

ext_inverter = external_inverter.SolaredgeExternalInverter(
ext_inverter = SolaredgeExternalInverter(
dev.device_config.id, component_config, dev.client)
return ext_inverter.read_state()

def create_inverter(modbus_id: int) -> inverter.SolaredgeInverter:
def create_inverter(modbus_id: int) -> SolaredgeInverter:
component_config = SolaredgeInverterSetup(id=num,
configuration=SolaredgeInverterConfiguration(modbus_id=modbus_id))
return inverter.SolaredgeInverter(dev.device_config.id, component_config, dev.client)
return SolaredgeInverter(dev.device_config.id, component_config, dev.client)

log.debug("Solaredge IP: "+ip_address+":"+str(port))
log.debug("Solaredge Slave-IDs: ["+str(slave_id0)+", "+str(slave_id1)+", "+str(slave_id2)+", "+str(slave_id3)+"]")
Expand Down
36 changes: 28 additions & 8 deletions packages/modules/solaredge/external_inverter.py
@@ -1,4 +1,5 @@
#!/usr/bin/env python3
import logging
from typing import Dict, Union

from dataclass_utils import dataclass_from_dict
Expand All @@ -7,35 +8,54 @@
from modules.common.component_type import ComponentDescriptor
from modules.common.fault_state import ComponentInfo
from modules.common.modbus import ModbusDataType
from modules.common.simcount import SimCounter
from modules.common.store import get_inverter_value_store
from modules.solaredge.config import SolaredgeExternalInverterSetup
from modules.solaredge.scale import scale_registers
from modules.solaredge.meter import SolaredgeMeterRegisters

log = logging.getLogger(__name__)


class SolaredgeExternalInverter:
def __init__(self,
device_id: int,
component_config: Union[Dict, SolaredgeExternalInverterSetup],
tcp_client: modbus.ModbusTcpClient_) -> None:
self.__device_id = device_id
self.component_config = dataclass_from_dict(SolaredgeExternalInverterSetup, component_config)
self.__tcp_client = tcp_client
self.__sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="pv")
self.registers = SolaredgeMeterRegisters(self.component_config.configuration.meter_id)
self.__store = get_inverter_value_store(self.component_config.id)
self.component_info = ComponentInfo.from_component_config(self.component_config)

def update(self, state: InverterState) -> None:
self.__store.set(state)

def read_state(self) -> InverterState:
unit = self.component_config.configuration.modbus_id
# 40380 = "Meter 2/Total Real Power (sum of active phases)" (Watt)
power = self.__tcp_client.read_holding_registers(40380, ModbusDataType.INT_16, unit=unit)
_, exported = self.__sim_counter.sim_count(power)
def read_scaled_int16(address: int, count: int):
return scale_registers(
self.__tcp_client.read_holding_registers(
address,
[ModbusDataType.INT_16] * (count + 1),
unit=self.component_config.configuration.modbus_id)
)

def read_scaled_uint32(address: int, count: int):
return scale_registers(
self.__tcp_client.read_holding_registers(
address,
[ModbusDataType.UINT_32] * count + [ModbusDataType.INT_16],
unit=self.component_config.configuration.modbus_id)
)

with self.__tcp_client:
power = read_scaled_int16(self.registers.powers, 4)[0]
exported = read_scaled_uint32(self.registers.imp_exp, 8)[0]
currents = read_scaled_int16(self.registers.currents, 3)

return InverterState(
exported=exported,
power=power
power=power,
currents=currents
)


Expand Down
44 changes: 44 additions & 0 deletions packages/modules/solaredge/meter.py
@@ -0,0 +1,44 @@
class SolaredgeMeterRegisters:
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
# 40191/40192/40193: AC Current by phase
# 40194: AC Current Scale Factor
self.currents = 40191
# 40196/40197/40198: Voltage per phase
# 40203: AC Voltage Scale Factor
self.voltages = 40196
# 40204: AC Frequency
# 40205: AC Frequency Scale Factor
self.frequency = 40204
# 40222/40223/40224: Power factor by phase (unit=%)
# 40225: AC Power Factor Scale Factor
self.power_factors = 40222
# 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
# 40155: C_Option Export + Import, Production, consumption,
self.option = 40155
self._update_offset_meter_id(internal_meter_id)
self._update_offset_synergy_units(synergy_units)

def _update_offset_meter_id(self, meter_id: int) -> None:
OFFSET = [0, 174, 348]
self._add_offset(OFFSET[meter_id-1])

def _update_offset_synergy_units(self, synergy_units: int) -> None:
"""https://www.solaredge.com/sites/default/files/sunspec-implementation-technical-note.pdf:
For 2-unit three phase inverters with Synergy technology, add 50 to the default addresses.
For 3-unit three phase inverters with Synergy technology, add 70 to the default addresses.
"""
OFFSET = [0, 50, 70]
self._add_offset(OFFSET[synergy_units-1])

def _add_offset(self, offset: int) -> None:
for name, value in self.__dict__.items():
setattr(self, name, value+offset)

0 comments on commit 11ce0a7

Please sign in to comment.