Skip to content
Open
Show file tree
Hide file tree
Changes from 25 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)
67 changes: 67 additions & 0 deletions packages/modules/devices/anker/anker_solix/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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, energy_meter: bool = True, modbus_id: int = 1):
self.energy_meter = energy_meter
self.modbus_id = modbus_id


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, mppt: bool = False, modbus_id: int = 1):
self.mppt = mppt
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)
51 changes: 51 additions & 0 deletions packages/modules/devices/anker/anker_solix/inverter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/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, eine AC Leistung gibt es so nicht
power = self.client.read_input_registers(10002, ModbusDataType.INT_32,
wordorder=Endian.Little, unit=unit) * -1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Der Wechselrichter kann mit der DC-Leistung leider nicht angebunden werden, da alle anderen Leistungen AC sind und dann nicht miteinander verrechnet werden können.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ja ist mir bewusst Lena, es gibt noch eine "Load Power" Register 10010 die die AC Leistung sein könnte, aber mangels Hardware kann ich es halt leider nicht testen. Kann man halt dann ggf noch anpassen wenn es mal User mit dem Setup gibt. Es kann aber auch ein ganz anderer Wert sein, daher erscheint es mir dennoch am besten erstmal die PV Leistung zu nehmen?
https://github.com/anker-charging/ha-anker-solix-official/blob/5eb27a1a4616afc4786d0171dd574f0a24224852/custom_components/anker_solix_official/config/8fcbb87c685781b1d70d784a79eb923098955df2aaf199095ce7767bb70b913d.yaml#L127

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dann nimm Register 10002 für dc_power und "Load Power" für power. Dann ist zumindest das DC-Power-Register an der richtigen Stelle.
Klar, ohne Hardware ist das schwierig für Dich. Eigentlich sollte es recht schnell auffallen, weil Hausverbrauch, Ladekosten, .. nicht passen, aber das wird nicht unbedingt so klar mit einer nicht plausiblen PV-Leistung in Verbindung gebracht.

Copy link
Copy Markdown
Contributor Author

@seaspotter seaspotter Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mach ich, aber dann solltet ihr anderswo auch konsequent sein wenn keine DC Leistung als "power" verwendet werden soll :) Siehe: https://github.com/openWB/core/blob/master/packages/modules/devices/solakon/solakon_one/inverter.py#L32
Oder bei Victron wenn die Leistung der MPPTs hergenommen wird, ist das auch nur DC Leistung beim inverter :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Danke für den Hinweis. Wir sehen uns die vorhandenen Module mittelfristig nochmal an. Kann gut sein, dass in der Vergangenheit da nicht so sauber gearbeitet wurde.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Danke für den Hinweis. Wir sehen uns die vorhandenen Module mittelfristig nochmal an. Kann gut sein, dass in der Vergangenheit da nicht so sauber gearbeitet wurde.

Gerne, vllt ist es ja ein denkbarer Ansatz wenns nur die PV Leistung gibt diese zwar im Modul als "dc_power" anzugeben, wenn keine "power" vorhanden ist, ersatzweise dann aber "dc_power" für die Anzeige herzunehmen mit ggf. Infomeldung?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wenn Ströme aus der Wirkleistung berechnet werden, sollte die Ungenauigkeit nicht sehr groß sein, da ein WR in der Regel nur sehr wenig Blindleistung produziert. Bei der Wandlung von DC nach AC hingegen treten größere Verluste auf. Die sollte man nicht vernachlässigen.

Copy link
Copy Markdown
Contributor Author

@seaspotter seaspotter Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Der Kommentar passt irgendwie gar nicht in die Diskussion, was haben die Ströme mit der Leistung zu tun?Es geht ja darum wie verfahren wird wenn eben (wie beim Solakon oder auch bei den Victron MPPTs) gar keine AC Leistung als Register vorhanden ist. So hat Andreas das aber jetzt auch mit dem aktuellen PR (#3294) eingebunden, weil es nur die DC Leistung gibt. Soll laut euch ja aber eigentlich nicht sein?
Wenn es aber keine AC Leistung gibt, ist es doch immer noch besser die DC Leistung zu nehmen statt gar nichts? So ist (vermute ich) auch die Denkweise bei Andreas dazu gewesen. Nur sauberer wäre es ja das dann auch als dc_power zu deklarieren und wenn in einem Invertermodul kein "power" vorhanden ist, dann eben ersatzweise "dc_power" zu nehmen für die Anzeige der Leistung, statt gar nichts. Und dann eben vllt noch als Infomeldung im Modul zu schreiben: "Achtung der Wechselrichter gibt als Leistung nur die PV Leistung aus und kann somit in der Leistungsbilanz etwas abweichen".
Um das noch zu verfollständigen: auch bei allen DC angebundenen Batterien ist "power" immer eine DC Leistung und niemals eine AC Leistung, auch das ist in der Leistungsbilanz nicht ganz sauber :) Aber vermutlich vernachlässigbar.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wie schon geschrieben, wir sehen uns das generell nochmal an. Aktuell passt das zeitlich mit den Vorbereitungen des Release 2.2.0 leider nicht.

self.peak_filter.check_values(power)
imported, exported = self.sim_counter.sim_count(power)
inverter_state = InverterState(
power=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