Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1ba739b
Add Vendor class for Anker devices
seaspotter Apr 14, 2026
ec0f1e2
Add files via upload
seaspotter Apr 14, 2026
5ee0335
Add Anker configuration and setup classes
seaspotter Apr 14, 2026
ef13f0b
Add files via upload
seaspotter Apr 14, 2026
92489b8
Add Anker device implementation in device.py
seaspotter Apr 14, 2026
6fd9196
Add AnkerInverter class for inverter management
seaspotter Apr 14, 2026
e0c7c60
Add AnkerCounterVersion enum for device versions
seaspotter Apr 14, 2026
ca326bb
Add AnkerCounter class with initialization and update methods
seaspotter Apr 14, 2026
395ecbb
Delete packages/modules/devices/anker/anker_solix/version.py
seaspotter Apr 14, 2026
2113e2d
Add AnkerBat class for battery management
seaspotter Apr 14, 2026
31e7f91
Refactor Modbus client usage in AnkerBat class
seaspotter Apr 14, 2026
90b49ac
Refactor counter.py to use SimCounter and update client
seaspotter Apr 14, 2026
c2dcb76
Refactor inverter.py to use ModbusTcpClient_ directly
seaspotter Apr 14, 2026
5f6cb8b
Add device_id to KwargsDict in AnkerBat
seaspotter Apr 14, 2026
68e7ee2
Add device_id to KwargsDict and initialize in AnkerCounter
seaspotter Apr 14, 2026
df1aa1b
Add device_id to KwargsDict and initialize
seaspotter Apr 14, 2026
a98267a
Change default modbus_id from 100 to 1
seaspotter Apr 14, 2026
9c8a84a
Update power limit handling and component descriptor
seaspotter Apr 14, 2026
c2a0cc5
Refactor imports and update read_input_registers calls
seaspotter Apr 14, 2026
45cd964
Refactor imports and clean up inverter.py
seaspotter Apr 14, 2026
4ed6b49
flake8
seaspotter Apr 14, 2026
85196ee
Remove unused import from inverter.py
seaspotter Apr 14, 2026
29a3adc
flake8
seaspotter Apr 14, 2026
86872f2
flake8
seaspotter Apr 14, 2026
65f8d71
flake8
seaspotter Apr 14, 2026
a15005d
Add IP config for counter
seaspotter Apr 14, 2026
5fcbf47
flake8
seaspotter Apr 14, 2026
18f0665
flake8
seaspotter Apr 14, 2026
85476de
flake8
seaspotter Apr 14, 2026
737c316
Add dc_power
seaspotter Apr 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
Empty file.
79 changes: 79 additions & 0 deletions packages/modules/devices/anker/anker_solix/bat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
import logging
from typing import Any, Optional, TypedDict

from modules.common.abstract_device import AbstractBat
from modules.common.component_state import BatState
from modules.common.component_type import ComponentDescriptor
from modules.common.fault_state import ComponentInfo, FaultState
from modules.common.modbus import ModbusDataType, Endian, ModbusTcpClient_
from modules.common.simcount import SimCounter
from modules.common.store import get_bat_value_store
from modules.devices.anker.anker_solix.config import AnkerBatSetup
from modules.common.utils.peak_filter import PeakFilter
from modules.common.component_type import ComponentType

log = logging.getLogger(__name__)


class KwargsDict(TypedDict):
device_id: int
client: ModbusTcpClient_


class AnkerBat(AbstractBat):
def __init__(self, component_config: AnkerBatSetup, **kwargs: Any) -> None:
self.component_config = component_config
self.kwargs: KwargsDict = kwargs

def initialize(self) -> None:
self.__device_id: int = self.kwargs['device_id']
self.client: ModbusTcpClient_ = self.kwargs['client']
self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="speicher")
self.store = get_bat_value_store(self.component_config.id)
self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config))
self.peak_filter = PeakFilter(ComponentType.BAT, self.component_config.id, self.fault_state)
self.last_mode = 'Undefined'

def update(self) -> None:
unit = self.component_config.configuration.modbus_id

power = self.client.read_input_registers(10008, ModbusDataType.INT_32,
wordorder=Endian.Little, unit=unit) * -1
soc = self.client.read_input_registers(10014, ModbusDataType.UINT_16, unit=unit)

self.peak_filter.check_values(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)

def set_power_limit(self, power_limit: Optional[int]) -> None:
unit = self.component_config.configuration.modbus_id

if power_limit is None:
log.debug("Keine Batteriesteuerung, Selbstregelung durch Wechselrichter")
if self.last_mode is not None:
self.client.write_register(10064, 0, data_type=ModbusDataType.UINT_16, unit=unit)
self.last_mode = None
else:
if self.last_mode != 'limited':
self.client.write_register(10064, 3, data_type=ModbusDataType.UINT_16, unit=unit)
self.last_mode = 'limited'

# Berechne power value: 0 = stop, != 0 = multipliziere mit -1
# Laut Doku ist der min Wert 100W, ggf. noch Anpassung für power_limit=0 notwendig

power_value = 0 if power_limit == 0 else int(power_limit) * -1
self.client.write_register(10071, power_value, data_type=ModbusDataType.INT_32, unit=unit)
log.debug("Aktive Batteriesteuerung angefordert, angeforderte Leistung: {power_value} W")

def power_limit_controllable(self) -> bool:
return True


component_descriptor = ComponentDescriptor(configuration_factory=AnkerBatSetup)
70 changes: 70 additions & 0 deletions packages/modules/devices/anker/anker_solix/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from typing import Optional

from modules.common.component_setup import ComponentSetup
from ..vendor import vendor_descriptor


class AnkerConfiguration:
def __init__(self, ip_address: Optional[str] = None, port: int = 502):
self.ip_address = ip_address
self.port = port


class Anker:
def __init__(self,
name: str = "Anker",
type: str = "anker",
id: int = 0,
configuration: AnkerConfiguration = None) -> None:
self.name = name
self.type = type
self.vendor = vendor_descriptor.configuration_factory().type
self.id = id
self.configuration = configuration or AnkerConfiguration()


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


class AnkerBatSetup(ComponentSetup[AnkerBatConfiguration]):
def __init__(self,
name: str = "Anker Speicher",
type: str = "bat",
id: int = 0,
configuration: AnkerBatConfiguration = None) -> None:
super().__init__(name, type, id, configuration or AnkerBatConfiguration())


class AnkerCounterConfiguration:
def __init__(self,
modbus_id: int = 1,
ip_address: Optional[str] = None,
port: int = 502):
self.modbus_id = modbus_id
self.ip_address = ip_address
self.port = port


class AnkerCounterSetup(ComponentSetup[AnkerCounterConfiguration]):
def __init__(self,
name: str = "Anker Zähler",
type: str = "counter",
id: int = 0,
configuration: AnkerCounterConfiguration = None) -> None:
super().__init__(name, type, id, configuration or AnkerCounterConfiguration())


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


class AnkerInverterSetup(ComponentSetup[AnkerInverterConfiguration]):
def __init__(self,
name: str = "Anker Wechselrichter",
type: str = "inverter",
id: int = 0,
configuration: AnkerInverterConfiguration = None) -> None:
super().__init__(name, type, id, configuration or AnkerInverterConfiguration())
62 changes: 62 additions & 0 deletions packages/modules/devices/anker/anker_solix/counter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
from typing import Any, TypedDict

from modules.common.abstract_device import AbstractCounter
from modules.common.component_state import CounterState
from modules.common.component_type import ComponentDescriptor
from modules.common.fault_state import ComponentInfo, FaultState
from modules.common.modbus import ModbusDataType, Endian, ModbusTcpClient_
from modules.common.simcount import SimCounter
from modules.common.store import get_counter_value_store
from modules.devices.anker.anker_solix.config import AnkerCounterSetup
from modules.common.utils.peak_filter import PeakFilter
from modules.common.component_type import ComponentType


class KwargsDict(TypedDict):
device_id: int
client: ModbusTcpClient_


class AnkerCounter(AbstractCounter):
def __init__(self, component_config: AnkerCounterSetup, **kwargs: Any) -> None:
self.component_config = component_config
self.kwargs: KwargsDict = kwargs

def initialize(self) -> None:
self.__device_id: int = self.kwargs['device_id']
self.client: ModbusTcpClient_ = self.kwargs['client']
self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="bezug")
self.store = get_counter_value_store(self.component_config.id)
self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config))
self.peak_filter = PeakFilter(ComponentType.COUNTER, self.component_config.id, self.fault_state)

def update(self):
unit = self.component_config.configuration.modbus_id

power = self.client.read_input_registers(10644, ModbusDataType.INT_32,
wordorder=Endian.Little, unit=unit) * -1
powers = self.client.read_input_registers(10638, [ModbusDataType.INT_32] * 3,
wordorder=Endian.Little, unit=unit)
voltages = self.client.read_input_registers(10632, [ModbusDataType.UINT_16] * 3,
wordorder=Endian.Little, unit=unit)
currents = self.client.read_input_registers(10666, [ModbusDataType.INT_16] * 3,
wordorder=Endian.Little, unit=unit)

voltages = [value / 10 for value in voltages]
currents = [value / -100 for value in currents]

self.peak_filter.check_values(power)
imported, exported = self.sim_counter.sim_count(power)
counter_state = CounterState(
imported=imported,
exported=exported,
power=power,
powers=powers,
voltages=voltages,
currents=currents
)
self.store.set(counter_state)


component_descriptor = ComponentDescriptor(configuration_factory=AnkerCounterSetup)
55 changes: 55 additions & 0 deletions packages/modules/devices/anker/anker_solix/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
import logging
from typing import Iterable, Union

from modules.common.abstract_device import DeviceDescriptor
from modules.common.component_context import SingleComponentUpdateContext
from modules.common.configurable_device import ComponentFactoryByType, ConfigurableDevice, MultiComponentUpdater
from modules.common.modbus import ModbusTcpClient_
from modules.devices.anker.anker_solix.bat import AnkerBat
from modules.devices.anker.anker_solix.config import Anker, AnkerBatSetup, AnkerCounterSetup, AnkerInverterSetup
from modules.devices.anker.anker_solix.counter import AnkerCounter
from modules.devices.anker.anker_solix.inverter import AnkerInverter

log = logging.getLogger(__name__)


def create_device(device_config: Anker):
client = None

def create_bat_component(component_config: AnkerBatSetup):
nonlocal client
return AnkerBat(component_config, device_id=device_config.id, client=client)

def create_counter_component(component_config: AnkerCounterSetup):
nonlocal client
return AnkerCounter(component_config, device_id=device_config.id, client=client)

def create_inverter_component(component_config: AnkerInverterSetup):
nonlocal client
return AnkerInverter(component_config, device_id=device_config.id, client=client)

def update_components(components: Iterable[Union[AnkerBat, AnkerCounter, AnkerInverter]]):
nonlocal client
with client:
for component in components:
with SingleComponentUpdateContext(component.fault_state):
component.update()

def initializer():
nonlocal client
client = ModbusTcpClient_(device_config.configuration.ip_address, device_config.configuration.port)

return ConfigurableDevice(
device_config=device_config,
initializer=initializer,
component_factory=ComponentFactoryByType(
bat=create_bat_component,
counter=create_counter_component,
inverter=create_inverter_component,
),
component_updater=MultiComponentUpdater(update_components)
)


device_descriptor = DeviceDescriptor(configuration_factory=Anker)
55 changes: 55 additions & 0 deletions packages/modules/devices/anker/anker_solix/inverter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
from typing import TypedDict, Any

from modules.common.abstract_device import AbstractInverter
from modules.common.component_state import InverterState
from modules.common.component_type import ComponentDescriptor
from modules.common.fault_state import ComponentInfo, FaultState
from modules.common.modbus import ModbusDataType, Endian, ModbusTcpClient_
from modules.common.simcount import SimCounter
from modules.common.store import get_inverter_value_store
from modules.devices.anker.anker_solix.config import AnkerInverterSetup
from modules.common.utils.peak_filter import PeakFilter
from modules.common.component_type import ComponentType


class KwargsDict(TypedDict):
device_id: int
client: ModbusTcpClient_


class AnkerInverter(AbstractInverter):
def __init__(self, component_config: AnkerInverterSetup, **kwargs: Any) -> None:
self.component_config = component_config
self.kwargs: KwargsDict = kwargs

def initialize(self) -> None:
self.__device_id: int = self.kwargs['device_id']
self.client: ModbusTcpClient_ = self.kwargs['client']
self.sim_counter = SimCounter(self.__device_id, self.component_config.id, prefix="pv")
self.store = get_inverter_value_store(self.component_config.id)
self.fault_state = FaultState(ComponentInfo.from_component_config(self.component_config))
self.peak_filter = PeakFilter(ComponentType.INVERTER, self.component_config.id, self.fault_state)

def update(self) -> None:
unit = self.component_config.configuration.modbus_id

# Register 10002 ist die PV_power also die DC Leistung
# Register 10010 ist "Load_power" unklar ob dies wirklich die AC Leistung des Inverters ist
power = self.client.read_input_registers(10010, ModbusDataType.INT_32,
wordorder=Endian.Little, unit=unit) * -1
dc_power = self.client.read_input_registers(10002, ModbusDataType.INT_32,
wordorder=Endian.Little, unit=unit) * -1

self.peak_filter.check_values(power)
imported, exported = self.sim_counter.sim_count(power)
inverter_state = InverterState(
power=power,
dc_power=dc_power,
imported=imported,
exported=exported
)
self.store.set(inverter_state)


component_descriptor = ComponentDescriptor(configuration_factory=AnkerInverterSetup)
14 changes: 14 additions & 0 deletions packages/modules/devices/anker/vendor.py
Original file line number Diff line number Diff line change
@@ -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 = "Anker"
self.group = VendorGroup.VENDORS.value


vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor)
Loading