Skip to content

Add Universeller Modbus#3699

Draft
AlexanderHa98 wants to merge 5 commits into
openWB:masterfrom
AlexanderHa98:feature_universelles_modbus_device
Draft

Add Universeller Modbus#3699
AlexanderHa98 wants to merge 5 commits into
openWB:masterfrom
AlexanderHa98:feature_universelles_modbus_device

Conversation

@AlexanderHa98

@AlexanderHa98 AlexanderHa98 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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.

Pull request overview

Adds a new “Universeller Modbus” device module under packages/modules/devices/generic/modbus to support configurable Modbus TCP-based counter, inverter, and battery components (configurable register mapping, datatype and endianness), aligned with the referenced UI/ticket work.

Changes:

  • Introduces a configurable Modbus TCP device (device.py) that instantiates counter/bat/inverter components.
  • Adds component implementations for counter, inverter, and battery that read values via Modbus input registers and publish *State to the stores.
  • Adds configuration models (config.py) and a small validation helper (helper.py) for register configuration completeness.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/modules/devices/generic/modbus/device.py New configurable device wiring for Modbus TCP client + component creation/update.
packages/modules/devices/generic/modbus/config.py New device/component configuration classes for universal Modbus mapping.
packages/modules/devices/generic/modbus/helper.py Adds check_data() validation for register config completeness.
packages/modules/devices/generic/modbus/counter.py New generic Modbus counter component reading power/energy and optional phase metrics.
packages/modules/devices/generic/modbus/inverter.py New generic Modbus inverter component reading power/energy and optional currents/DC power.
packages/modules/devices/generic/modbus/bat.py New generic Modbus battery component reading power/energy and optional SOC/currents.
Comments suppressed due to low confidence (3)

packages/modules/devices/generic/modbus/config.py:32

  • gerneric_modbus ist als mutable Dataclass mit nicht-optional typisierten Feldern deklariert, wird aber überall mit None initialisiert. Dadurch stimmen Typen/Defaults nicht, und als Default-Argument wiederverwendete Instanzen können unbeabsichtigt geteilt werden. Optional-Felder mit Defaults (und idealerweise Immutability) vermeiden das.
@dataclass
class gerneric_modbus:
    reg_address: int
    reg_type: str
    byteorder: str
    wordorder: str

packages/modules/devices/generic/modbus/bat.py:137

  • Optionalfelder werden nachträglich per locals() am BatState gesetzt. Dadurch werden Defaulting/Validierungen in BatState.__init__ (z.B. Strom-Sign-Prüfung) umgangen. Besser: Optionalwerte direkt im Konstruktor übergeben.
        bat_state = BatState(
            imported=imported,
            exported=exported,
            power=power,
        )

packages/modules/devices/generic/modbus/counter.py:226

  • Beim Simulieren von importiert/exportiert werden die simulierten Zählerstände erneut durch den PeakFilter geschickt. Der PeakFilter kann dabei (z.B. beim Startup) bewusst None zurückgeben und so die simulierten Werte verwerfen. In anderen Modulen wird bei SimCounter nur die Leistung geprüft und die Zähler nicht gefiltert.
        if power is not None:
            self.peak_filter.check_values(power)
            if imported is None or exported is None:
                imported, exported = self.sim_counter.sim_count(power)
            imported, exported = self.peak_filter.check_values(power, imported, exported)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +8 to +11
class GenericModbusConfiguration:
def __init__(self, ip_address: str = "192.168.1.230", port: int = 502):
self.ip_address = ip_address
self.port = port
Comment on lines +125 to +129
if power is not None:
self.peak_filter.check_values(power)
if imported is None or exported is None:
imported, exported = self.sim_counter.sim_count(power)
imported, exported = self.peak_filter.check_values(power, imported, exported)

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.

Bitte noch umsetzen.

Comment on lines +131 to +142
inverter_state = InverterState(
power=power,
exported=exported,
imported=imported,
)

if "dc_power" in locals():
inverter_state.dc_power = dc_power
if "currents" in locals():
inverter_state.currents = currents
if 'serial_number' in locals():
inverter_state.serial_number = serial_number
Comment on lines +127 to +131
if power is not None:
self.peak_filter.check_values(power)
if imported is None or exported is None:
imported, exported = self.sim_counter.sim_count(power)
imported, exported = self.peak_filter.check_values(power, imported, exported)
Comment on lines +228 to +245
counter_state = CounterState(
imported=imported,
exported=exported,
power=power
)

if "voltages" in locals():
counter_state.voltages = voltages
if "currents" in locals():
counter_state.currents = currents
if 'powers' in locals():
counter_state.powers = powers
if "power_factors" in locals():
counter_state.power_factors = power_factors
if 'frequency' in locals():
counter_state.frequency = frequency
if 'serial_number' in locals():
counter_state.serial_number = serial_number
byteorder=self.component_config.configuration.powers_L3.byteorder,
wordorder=self.component_config.configuration.powers_L3.wordorder, unit=unit)

# Power Factors‚
Comment on lines +29 to +34
self.__modbus_id: int = self.kwargs['device_id']
self.client: ModbusTcpClient_ = self.kwargs['client']
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.sim_counter = SimCounter(self.__modbus_id, self.component_config.id, prefix="speicher")

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.

Bitte noch umsetzen.

@AlexanderHa98
AlexanderHa98 force-pushed the feature_universelles_modbus_device branch from d43ce0c to bdd9550 Compare July 23, 2026 09:17
@AlexanderHa98
AlexanderHa98 force-pushed the feature_universelles_modbus_device branch from bdd9550 to adc6fed Compare July 23, 2026 09:28
def initialize(self) -> None:
self.__modbus_id: int = self.kwargs['device_id']
self.client: ModbusTcpClient_ = self.kwargs['client']
self.store = get_bat_value_store(self.component_config.id)

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.

Suggested change
self.store = get_bat_value_store(self.component_config.id)
self.store = get_component_value_store(self.component_config.type, self.component_config.id)

Der Aufruf sieht einheitlich für alle Komponenten so aus.

if self.component_config.configuration.power.reg_address is not None:
check_data(self.component_config.configuration.power)
power = self.client.read_input_registers(
int(self.component_config.configuration.power.reg_address), ModbusDataType[

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.

Suggested change
int(self.component_config.configuration.power.reg_address), ModbusDataType[
self.component_config.configuration.power.reg_address, ModbusDataType[

reg_address und die anderen Adressen sollten doch schon ein int sein.

Comment on lines +56 to +60
soc = self.client.read_input_registers(
int(self.component_config.configuration.soc.reg_address), ModbusDataType[
self.component_config.configuration.soc.reg_type],
byteorder=self.component_config.configuration.soc.byteorder,
wordorder=self.component_config.configuration.soc.wordorder, unit=unit)

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.

Man könnte den Aufruf in eine eigene Methode oder Nested Function packen und self.component_config.configuration.soc als Variable übergeben. Der Typ ist ja immer gerneric_modbus.



@dataclass
class gerneric_modbus:

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.

Bitte an die Naming Convention für Klassennamen halten. Hier würde zB RegisterConfig oä passen.

serial_number: gerneric_modbus = gerneric_modbus(
reg_address=None, reg_type=None, byteorder=None, wordorder=None)):

self.modbus_id = modbus_id

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.

GenericModbusCounterConfiguration kannst Du als dataclass anlegen, dann musst Du nach init nicht nochmal alle Parameter zuweisen.


class GenericModbusCounterConfiguration:
def __init__(self, modbus_id: int = 105,
voltage_L1: gerneric_modbus = gerneric_modbus(

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 Du gerneric_modbus Default-Parameter spendierst, kannst Du sie hier weglassen, dann ist die Liste nicht so lang.

if (self.component_config.configuration.voltage_L1.reg_address is not None or
self.component_config.configuration.voltage_L2.reg_address is not None or
self.component_config.configuration.voltage_L3.reg_address is not None):
# Kein register angegeben

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 Kommentar ist nicht mehr aktuell.

@AlexanderHa98
AlexanderHa98 requested a review from LKuemmel July 23, 2026 14:10
Comment on lines +48 to +49
if soc_value is not None:
soc = soc_value

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.

Suggested change
if soc_value is not None:
soc = soc_value

Unten setzt Du auch auf None, wenn es die Variable nicht in locals gibt. Das ist dann doppelt.

f"Unvollständige Konfiguration für Universeller-Modbus: Register-Adresse {wert.reg_address}")


def read_value(client: ModbusTcpClient_, unit: int, register_config: Any) -> Any:

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.

Suggested change
def read_value(client: ModbusTcpClient_, unit: int, register_config: Any) -> Any:
def read_value(client: ModbusTcpClient_, unit: int, register_config: RegisterConfig) -> Any:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants