Skip to content

Commit

Permalink
Merge pull request #2407 from LKuemmel/solaredge
Browse files Browse the repository at this point in the history
Solaredge: consider synergy units
  • Loading branch information
LKuemmel committed Sep 22, 2022
2 parents 21ca94f + 5935811 commit e937220
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 100 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
64 changes: 19 additions & 45 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 @@ -9,7 +10,10 @@
from modules.common.modbus import ModbusDataType
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.scale import create_scaled_reader
from modules.solaredge.meter import SolaredgeMeterRegisters

log = logging.getLogger(__name__)


class SolaredgeCounter:
Expand All @@ -19,55 +23,25 @@ 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)
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):
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)
)

# 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 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,
Expand Down
59 changes: 42 additions & 17 deletions packages/modules/solaredge/device.py
Expand Up @@ -2,7 +2,7 @@
import logging
from operator import add
from statistics import mean
from typing import Dict, Tuple, Union, Optional, List
from typing import Dict, Iterable, Tuple, Union, Optional, List
from urllib3.util import parse_url

try:
Expand All @@ -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.values(), 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: Iterable[solaredge_component_classes], synergy_units: int) -> None:
meters = [None]*3 # type: List[Union[SolaredgeExternalInverter, SolaredgeCounter, None]]
for component in components:
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(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
64 changes: 64 additions & 0 deletions packages/modules/solaredge/device_test.py
@@ -0,0 +1,64 @@
from typing import List, NamedTuple, Type
from unittest.mock import Mock

import pytest

from modules.solaredge import device
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


Params = NamedTuple("Params", [("configured_meter_ids", List[int]), ("effective_meter_ids", List[int])])


@pytest.mark.parametrize(["params"], [
pytest.param(
Params(configured_meter_ids=[1, 2], effective_meter_ids=[1, 2]),
id="ids unchanged if meter_ids are continuous starting from 1"
),
pytest.param(
Params(configured_meter_ids=[2, 3], effective_meter_ids=[1, 2]),
id="ids move forward if not starting at 1"
),
pytest.param(
Params(configured_meter_ids=[1, 3], effective_meter_ids=[1, 2]),
id="gaps in ids are closed"
)
])
def test_set_component_registers_assigns_effective_meter_ids(monkeypatch, params: Params):
# setup
monkeypatch.setattr(
device, "SolaredgeMeterRegisters", Mock(side_effect=lambda internal_meter_id, _: internal_meter_id)
)
components_list = [
Mock(spec=SolaredgeCounter, component_config=Mock(configuration=Mock(meter_id=meter_id)))
for meter_id in params.configured_meter_ids
]

# execution
device.Device.set_component_registers(components_list, synergy_units=1)

# evaluation
assert [component.registers for component in components_list] == params.effective_meter_ids


@pytest.mark.parametrize("type,should_use", [
(SolaredgeCounter, True),
(SolaredgeExternalInverter, True),
(SolaredgeBat, False),
(SolaredgeInverter, False),
])
def test_set_component_registers_ignores_wrong_types(monkeypatch, type: Type, should_use: bool):
# setup
monkeypatch.setattr(
device, "SolaredgeMeterRegisters", Mock(side_effect=lambda *args: True)
)
components = [Mock(spec=type, component_config=Mock(configuration=Mock(meter_id=1)))
]
# execution
device.Device.set_component_registers(components, synergy_units=1)

# evaluation
assert hasattr(components[0], "registers") == should_use
25 changes: 17 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,43 @@
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 create_scaled_reader
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)
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, 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)
power = self._read_scaled_int16(self.registers.powers, 4)[0]
exported = self._read_scaled_uint32(self.registers.imp_exp, 8)[0]
currents = self._read_scaled_int16(self.registers.currents, 3)

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


Expand Down

0 comments on commit e937220

Please sign in to comment.