Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 6 additions & 4 deletions grobro/grobro/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ def __on_message(self, client, userdata, msg: MQTTMessage):
self._client.publish(topic, smart_meter["data"], retain=PUBLISH_SENSORS_RETAINED)
return

# NOAH/NEXA-specific message types (FE19 config, 0103 holding regs, etc.)
# NOAH/NEXA-specific message types (FE19 config, etc.)
noah_msg = parser.parse_noah_message(unscrambled)
if noah_msg and noah_msg.get("message_type") == 0xFE19:
if device_id.startswith("0PVP") or device_id.startswith("0HVR"):
Expand Down Expand Up @@ -411,14 +411,16 @@ def __on_message(self, client, userdata, msg: MQTTMessage):
LOG.info("Modbus message from unknown device type: %s", device_id)
return

if (
modbus_message.function
== GrowattModbusFunction.READ_SINGLE_REGISTER
if modbus_message.function in (
GrowattModbusFunction.READ_SINGLE_REGISTER,
GrowattModbusFunction.READ_HOLDING_REGISTER,
):
state = HomeAssistantHoldingRegisterInput(device_id=modbus_device_id)

for name, register in known_registers.holding_registers.items():
data_raw = modbus_message.get_data(register.growatt.position)
if data_raw is None:
continue
value = register.growatt.data.parse(data_raw)
if value is None:
continue
Expand Down
20 changes: 0 additions & 20 deletions grobro/grobro/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,25 +167,6 @@ def parse_config_ack(data: bytes):
# The 2-byte type/subtype field starts at payload offset 14.


def parse_noah_0103(data: bytes) -> dict:
"""
NOAH type 0x0103 — Holding register dump.
Payload: 14 zero bytes + 16B device serial (offset 14) + register data.
"""
payload = data[24:]
device_id = payload[14:30].rstrip(b"\x00").decode("ascii", errors="replace")
reg_data = payload[30:]
registers = []
for i in range(0, len(reg_data) - 1, 2):
registers.append(struct.unpack_from(">H", reg_data, i)[0])
return {
"message_type": 0x0103,
"device_id": device_id,
"registers": registers,
"register_count": len(registers),
}


def parse_noah_0110(data: bytes) -> dict:
"""
NOAH type 0x0110 — Preset-multiple register response/ack.
Expand Down Expand Up @@ -315,7 +296,6 @@ def parse_noah_6f64(data: bytes) -> dict:


NOAH_DECODERS = {
0x0103: parse_noah_0103,
0x0110: parse_noah_0110,
0x0125: parse_noah_0125,
0xFE18: parse_noah_fe18,
Expand Down
9 changes: 7 additions & 2 deletions grobro/model/modbus_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class GrowattModbusMessage(BaseModel):
- B - 1 byte modbus device address (seems to be constant 1 in mqtt)
- B - 1 byte function
- 30s - 30 byte zero-padded device id
- optional GrowattModbusMetadata - only present when function == READ_INPUT_REGISTER
- optional GrowattModbusMetadata - present for functions READ_INPUT_REGISTER and READ_HOLDING_REGISTER
- N register blocks
"""

Expand Down Expand Up @@ -163,12 +163,17 @@ def parse_grobro(buffer) -> Optional["GrowattModbusMessage"]:
offset = 38

metadata = None
if function == GrowattModbusFunction.READ_INPUT_REGISTER:
if function in (
GrowattModbusFunction.READ_INPUT_REGISTER,
GrowattModbusFunction.READ_HOLDING_REGISTER,
):
metadata = GrowattMetadata.parse_grobro(buffer[offset:])
offset += metadata.size()

while len(buffer) > offset + 6:
block = GrowattModbusBlock.parse_grobro(buffer[offset:])
if block is None:
return None
register_blocks.append(block)
offset += block.size()

Expand Down
12 changes: 8 additions & 4 deletions tests/model/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from grobro.grobro import parser
from grobro.model.modbus_message import (
GrowattModbusFunction,
GrowattModbusMessage,
)
from grobro.model.modbus_function import GrowattModbusFunctionSingle
Expand Down Expand Up @@ -229,10 +230,13 @@ def test_noah_preset_multiple_charge_limit():
def test_noah_type0103_holding_registers():
data = (DATA_DIR / "NoahType0103_HoldingRegs.bin").read_bytes()
unscrambled = parser.unscramble(data)
result = parser.parse_noah_0103(unscrambled)
assert result["message_type"] == 0x0103
assert result["device_id"] == NOAH_TEST_DEVICE_ID
assert result["register_count"] > 0
result = GrowattModbusMessage.parse_grobro(unscrambled)
assert result is not None
assert result.device_id == NOAH_TEST_DEVICE_ID
assert result.function == GrowattModbusFunction.READ_HOLDING_REGISTER
assert result.metadata is not None
assert result.metadata.device_sn == NOAH_TEST_DEVICE_ID
assert len(result.register_blocks) > 0


def test_noah_type0110_response():
Expand Down