"""Client-visible EcoFlow protocol fields, using synthetic reports only.""" import io import struct from unittest.mock import MagicMock, patch import pytest from bmspy.classes import BMSInfoField, BMSScalarField from bmspy.client import socket_comms from bmspy.ecoflow_bt import EcoFlowBTDevice, EcoFlowPacket from bmspy.ecoflow_river2 import MESSAGE_DEFINITIONS def device(prefix): return EcoFlowBTDevice("02:00:00:00:00:01", prefix + "TEST00000001", "test-user") def record(dev, payload, route=(2, 0xFE, 0x15)): src, cmd_set, cmd_id = route dev._record(EcoFlowPacket(src, 0x21, cmd_set, cmd_id, payload)) return dict(dev.snapshot.items()) def heartbeat(definitions): # Include skipped bytes, zeroes, and signed values, not just summary fields. values = [bytes([0xAB]) * struct.calcsize("<" + f.fmt) if f.fmt.endswith("s") else -3 if f.fmt in ("i", "h") else 1.5 if f.fmt == "f" else 0 for f in definitions] return struct.pack("<" + "".join(f.fmt for f in definitions), *values), values @pytest.mark.parametrize("route,namespace", [ ((2, 0x20, 2), "pd"), ((3, 0x20, 2), "ems"), ((3, 0x20, 0x32), "bms"), ((4, 0x20, 2), "inv"), ((5, 0x20, 2), "mppt"), ]) def test_every_river2_layout_field_is_client_visible(route, namespace): definitions = MESSAGE_DEFINITIONS[route][1] payload, values = heartbeat(definitions) fields = record(device("R601"), payload, route) prefix = f"ef_river2_{namespace}_" assert {key for key in fields if key.startswith(prefix)} == { prefix + f.name for f in definitions } for definition, value in zip(definitions, values): field = fields[prefix + definition.name] if isinstance(value, bytes): assert isinstance(field, BMSInfoField) assert field.info == "0x" + value.hex() else: assert isinstance(field, BMSScalarField) assert field.raw_value == value * definition.scale assert field.units == (definition.unit or None) def test_river2_short_firmware_reports_keep_previous_tail_and_explicit_zero(): dev = device("R603") definitions = MESSAGE_DEFINITIONS[(2, 0x20, 2)][1] payload, _ = heartbeat(definitions) record(dev, payload + b"\x12\x34", (2, 0x20, 2)) fields = record(dev, payload[:23], (2, 0x20, 2)) assert fields["ef_river2_pd_watts_in_sum"].raw_value == 0 assert fields["ef_river2_pd_relay_switch_count"].raw_value == 0 assert fields["ef_river2_pd_trailing_bytes"].info == "0x1234" assert fields["ef_river2_pd_remain_time"].raw_value == -3 assert "ef_remaining_time_minutes" not in fields fresh = record(device("R601"), payload[:23], (2, 0x20, 2)) assert "ef_river2_pd_relay_switch_count" not in fresh def test_river2_malformed_report_preserves_previous_protocol_fields(): dev = device("R601") definitions = MESSAGE_DEFINITIONS[(3, 0x20, 2)][1] payload, _ = heartbeat(definitions) record(dev, payload, (3, 0x20, 2)) for bad in (payload[:24], payload[:26] + struct.pack("