"""Client-visible EcoFlow protocol fields, using synthetic reports only.""" import io import json import struct from dataclasses import asdict from unittest.mock import MagicMock, patch import pytest from bmspy.classes import BMSInfoField, BMSScalarField from bmspy.client import read_data, socket_comms from bmspy.ecoflow_bt import EcoFlowBTDevice, EcoFlowPacket from bmspy.ecoflow_proto import decode_schema_values from bmspy.ecoflow_river2 import MESSAGE_DEFINITIONS from bmspy.ecoflow_river3_schema import ENUMS, MESSAGES from bmspy.server import send_response def varint(value): value &= (1 << 64) - 1 result = bytearray() while value > 127: result.append((value & 127) | 128) value >>= 7 result.append(value) return bytes(result) def integer(number, value): return varint(number << 3) + varint(value) def floating(number, value): return varint(number << 3 | 5) + struct.pack(" 65536 stream = io.BytesIO(frame) sock = MagicMock() sock.recv.side_effect = lambda n: stream.read(min(n, 2 if stream.tell() < 4 else 127)) with patch("bmspy.client.socket.socket", return_value=sock): result = read_data("/fake/socket", "test") expected = json.loads(frame[4:]) actual = {device: {key: asdict(value) for key, value in snap.items()} for device, snap in result.items()} assert actual == expected assert stream.read() == b"" sock.close.assert_called_once() @pytest.mark.parametrize("frame", [b"", b"\x00\x00", struct.pack("!I", 100) + b"{}"]) def test_incomplete_client_response_fails_without_looping(frame): sock = MagicMock() sock.recv.side_effect = io.BytesIO(frame).read with patch("bmspy.client.socket.socket", return_value=sock), pytest.raises(SystemExit): socket_comms("/fake/socket", {"command": "GET"}) sock.close.assert_called_once() def test_connection_failure_does_not_read_from_unconnected_socket(): sock = MagicMock() sock.connect.side_effect = ConnectionRefusedError("offline") with patch("bmspy.client.socket.socket", return_value=sock), pytest.raises(ConnectionRefusedError): socket_comms("/fake/socket", {"command": "GET"}) sock.recv.assert_not_called() sock.sendall.assert_not_called() sock.close.assert_called_once() def test_all_fields_reach_influxdb(): pytest.importorskip("influxdb_client_3") from bmspy.influxdb import influxdb_create_snapshot snapshots = full_snapshots() points = influxdb_create_snapshot(snapshots) expected = {(ups, key) for ups, snap in snapshots.items() for key, _ in snap.items()} assert {(point._tags["ups"], point._name) for point in points} == expected for point in points: field = dict(snapshots[point._tags["ups"]].items())[point._name] value = (field.info if isinstance(field, BMSInfoField) else field.raw_value if isinstance(field, BMSScalarField) else field.raw_values[1]) assert point._fields["value"] == value assert point.to_line_protocol() def test_all_fields_reach_prometheus(): prometheus = pytest.importorskip("prometheus_client") from bmspy.prometheus import prometheus_create_metric, prometheus_populate_metric snapshots = full_snapshots() registry = prometheus.CollectorRegistry() metrics = prometheus_create_metric(registry, snapshots) prometheus_populate_metric(metrics, snapshots) assert set(metrics) == {key for snap in snapshots.values() for key, _ in snap.items()} for ups, snap in snapshots.items(): for key, field in snap.items(): labels = {"ups": ups} if isinstance(field, BMSInfoField): labels[key] = field.info assert registry.get_sample_value(key + "_info", labels) == 1 elif isinstance(field, BMSScalarField): assert registry.get_sample_value(key, labels) == field.raw_value else: for index, value in field.raw_values.items(): assert registry.get_sample_value(key, {**labels, field.label: str(index)}) == value def test_prometheus_daemon_adds_fields_from_later_reports(): prometheus = pytest.importorskip("prometheus_client") from bmspy.prometheus import prometheus_export first, later = device("R651"), device("R651") record(first, floating(3, 10)) record(later, floating(3, 20) + floating(11, -14)) snapshots = [{"nas": first.snapshot}, {"nas": later.snapshot}] captured = [] def capture(port, registry): captured.append(registry) class Finished(Exception): pass with patch("bmspy.prometheus.client.read_data", side_effect=snapshots), \ patch.object(prometheus, "start_http_server", side_effect=capture), \ patch("bmspy.prometheus.time.sleep", side_effect=[None, Finished]): with pytest.raises(Finished): prometheus_export() registry = captured[0] assert registry.get_sample_value("ef_river3_display_pow_in_sum_w", {"ups": "nas"}) == 20 assert registry.get_sample_value("ef_river3_display_pow_get_typec1", {"ups": "nas"}) == -14