Wait for the required EcoFlow reports in each collection
A Bluetooth collection listened for a fixed 10 seconds and returned the data that arrived. If a required report was late, the snapshot missed shared measurements without notice. Each decoder now tracks the reports behind the shared measurements. A collection listens for 10 seconds, then until those reports arrive, for at most 30 seconds in total. The new ef_missing_measurements field names the reports that did not arrive. The server log shows one line each time that list changes. The diagnostic tool now uses the River 2 heartbeat list from the decoder.
This commit is contained in:
@@ -494,6 +494,7 @@ def test_collection_always_disconnects(monkeypatch, connect_ok, authenticated, p
|
||||
fake = Mock(
|
||||
connect=AsyncMock(return_value=connect_ok),
|
||||
wait_authenticated=AsyncMock(return_value=authenticated),
|
||||
wait_for_measurements=AsyncMock(return_value=set()),
|
||||
disconnect=AsyncMock(), _pkt_count=packets,
|
||||
)
|
||||
monkeypatch.setattr("bmspy.ecoflow_bt.EcoFlowBTDevice", Mock(return_value=fake))
|
||||
@@ -502,6 +503,104 @@ def test_collection_always_disconnects(monkeypatch, connect_ok, authenticated, p
|
||||
fake.disconnect.assert_awaited_once()
|
||||
|
||||
|
||||
def test_wait_ends_when_the_required_heartbeats_arrive(monkeypatch):
|
||||
monkeypatch.setattr("bmspy.ecoflow_bt._COMPLETION_POLL_SECONDS", 0.01)
|
||||
|
||||
async def run():
|
||||
dev = device()
|
||||
dev._record(heartbeat(2, pd_payload()))
|
||||
dev._record(heartbeat(3, ems_payload()))
|
||||
|
||||
async def late_bms():
|
||||
await asyncio.sleep(0.03)
|
||||
dev._record(heartbeat(3, bms_payload(), 0x32))
|
||||
|
||||
task = asyncio.create_task(late_bms())
|
||||
loop = asyncio.get_running_loop()
|
||||
start = loop.time()
|
||||
# The late heartbeat extends the wait past the minimum, far below the limit.
|
||||
missing = await dev.wait_for_measurements(0, 30)
|
||||
await task
|
||||
return missing, loop.time() - start
|
||||
|
||||
missing, elapsed = asyncio.run(run())
|
||||
assert missing == set()
|
||||
assert 0.03 <= elapsed < 5
|
||||
|
||||
|
||||
def test_wait_keeps_the_minimum_for_optional_reports(monkeypatch):
|
||||
monkeypatch.setattr("bmspy.ecoflow_bt._COMPLETION_POLL_SECONDS", 0.01)
|
||||
|
||||
async def run():
|
||||
dev = device()
|
||||
for packet in (heartbeat(2, pd_payload()), heartbeat(3, ems_payload()), heartbeat(3, bms_payload(), 0x32)):
|
||||
dev._record(packet)
|
||||
loop = asyncio.get_running_loop()
|
||||
start = loop.time()
|
||||
missing = await dev.wait_for_measurements(0.05, 30)
|
||||
return missing, loop.time() - start
|
||||
|
||||
missing, elapsed = asyncio.run(run())
|
||||
assert missing == set()
|
||||
assert elapsed >= 0.05
|
||||
|
||||
|
||||
@pytest.mark.parametrize("disconnected", [False, True])
|
||||
def test_wait_stops_at_the_limit_or_a_lost_connection(monkeypatch, disconnected):
|
||||
monkeypatch.setattr("bmspy.ecoflow_bt._COMPLETION_POLL_SECONDS", 0.01)
|
||||
|
||||
async def run():
|
||||
dev = device()
|
||||
dev._record(heartbeat(2, pd_payload()))
|
||||
if disconnected:
|
||||
dev._on_ble_disconnect(None)
|
||||
loop = asyncio.get_running_loop()
|
||||
start = loop.time()
|
||||
missing = await dev.wait_for_measurements(0, 30 if disconnected else 0.05)
|
||||
return missing, loop.time() - start
|
||||
|
||||
missing, elapsed = asyncio.run(run())
|
||||
assert missing == {"EMS heartbeat", "BMS heartbeat"}
|
||||
assert elapsed < 5
|
||||
|
||||
|
||||
def test_wait_uses_only_the_minimum_for_a_model_without_a_decoder():
|
||||
async def run():
|
||||
return await device(serial="XXXXTEST00000001").wait_for_measurements(0, 30)
|
||||
|
||||
assert asyncio.run(run()) == set()
|
||||
|
||||
|
||||
def test_collection_reports_missing_reports_and_logs_changes(monkeypatch, capsys):
|
||||
from bmspy import ecoflow_bt
|
||||
|
||||
monkeypatch.setattr(ecoflow_bt, "_last_missing", {})
|
||||
results = [{"EMS heartbeat", "BMS heartbeat"}, {"EMS heartbeat", "BMS heartbeat"}, set()]
|
||||
snapshots = []
|
||||
|
||||
def fake_device(**kwargs):
|
||||
snapshots.append(ecoflow_bt.EcoFlowSnapshot())
|
||||
return Mock(
|
||||
connect=AsyncMock(return_value=True), wait_authenticated=AsyncMock(return_value=True),
|
||||
wait_for_measurements=AsyncMock(return_value=results[len(snapshots) - 1]),
|
||||
disconnect=AsyncMock(), _pkt_count=1, snapshot=snapshots[-1],
|
||||
)
|
||||
|
||||
monkeypatch.setattr(ecoflow_bt, "EcoFlowBTDevice", fake_device)
|
||||
config = initialise_device(ADDRESS, SERIAL, "test-user", listen_duration=7, listen_timeout=20)
|
||||
logs = []
|
||||
for _ in results:
|
||||
assert collect_data(config) is snapshots[-1]
|
||||
logs.append(capsys.readouterr().out)
|
||||
assert [snap.ef_missing_measurements.info for snap in snapshots] == [
|
||||
"BMS heartbeat, EMS heartbeat", "BMS heartbeat, EMS heartbeat", "",
|
||||
]
|
||||
# One line for each change, not one for each collection.
|
||||
assert "collection incomplete; missing BMS heartbeat, EMS heartbeat" in logs[0]
|
||||
assert logs[1] == ""
|
||||
assert "collection complete again" in logs[2]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def diagnostic_script():
|
||||
path = Path(__file__).resolve().parents[1] / "tools" / "ecoflow-bt.py"
|
||||
|
||||
Reference in New Issue
Block a user