"""Server routing and caching tests with synthetic device identifiers.""" from unittest.mock import Mock import pytest from bmspy import server from bmspy.classes import BMSScalarField from bmspy.jbd_bms import JBDBMS from bmspy.ecoflow_bt import initialise_device ADDRESS = "02:00:00:00:00:01" SERIAL = "R601TEST00000001" SPEC = f"{ADDRESS}/{SERIAL}/test-user" @pytest.mark.parametrize("spec,name,path", [ (SPEC, SERIAL, SPEC), (f"battery@{SPEC}", "battery", SPEC), ("/tmp/serial-device", "serial-device", "/tmp/serial-device"), ("ports/ttyUSB0", "ttyUSB0", "ports/ttyUSB0"), ("office:ports/ttyUSB0", "office", "ports/ttyUSB0"), ("office:/tmp/serial-device", "office", "/tmp/serial-device"), ("/tmp/serial@device", "serial@device", "/tmp/serial@device"), ]) def test_parse_preserves_serial_paths_and_bluetooth_names(spec, name, path): assert server.parse_device(spec) == (name, path) assert server.is_ecoflow_device(path) is (path == SPEC) def test_parse_bluetooth_spec(): assert server.parse_ecoflow_spec(SPEC) == (ADDRESS, SERIAL, "test-user") assert server.parse_ecoflow_spec("02:00:00:aa:bb:01 / SERIAL / user") == ( "02:00:00:AA:BB:01", "SERIAL", "user", ) @pytest.mark.parametrize("spec", [ ADDRESS, f"{ADDRESS}/{SERIAL}", f"{ADDRESS}//test-user", f"{ADDRESS}/{SERIAL}/", f"{SPEC}/extra", "GG:00:00:00:00:01/SERIAL/test-user", ]) def test_invalid_spec_does_not_echo_account_details(spec): assert server.is_ecoflow_device(spec) with pytest.raises(ValueError) as error: server.parse_ecoflow_spec(spec) assert str(error.value) == "EcoFlow BT device spec must be ADDRESS/SERIAL/USERID" def test_existing_positional_device_state_arguments_remain_valid(): port, data = Mock(), JBDBMS() state = server.DeviceState(port, data, 123.0) assert state.ser is port assert state.data is data assert state.timestamp == 123.0 assert state.ecoflow is None def test_collection_dispatches_to_the_selected_backend(monkeypatch): serial_data, bluetooth_data = JBDBMS(), JBDBMS() for data in (serial_data, bluetooth_data): data.bms_capacity_charge_ratio = BMSScalarField( help="Battery Charge", raw_value=0.5, value="0.5", ) serial_read = Mock(return_value=serial_data) bluetooth_read = Mock(return_value=bluetooth_data) monkeypatch.setattr(server, "collect_data", serial_read) monkeypatch.setattr(server, "ecoflow_collect_data", bluetooth_read) port = Mock() config = initialise_device(ADDRESS, SERIAL, "test-user") assert server.collect_device_data(server.DeviceState(ser=port), 2) is serial_data serial_read.assert_called_once_with(port, 2) assert server.collect_device_data(server.DeviceState(ecoflow=config), 3) is bluetooth_data bluetooth_read.assert_called_once_with(config, 3) assert server.collect_device_data(server.DeviceState()) is None @pytest.mark.parametrize("bluetooth,interval", [(False, 5), (True, 60)]) def test_cache_interval_depends_on_backend(monkeypatch, bluetooth, interval): old, fresh = JBDBMS(), JBDBMS() config = initialise_device(ADDRESS, SERIAL, "test-user") if bluetooth else None state = server.DeviceState(data=old, timestamp=100, ecoflow=config) read = Mock(return_value=fresh) monkeypatch.setattr(server, "collect_device_data", read) monkeypatch.setattr(server.time, "time", lambda: 100 + interval - 1) assert server.get_device_data(state) is old read.assert_not_called() monkeypatch.setattr(server.time, "time", lambda: 100 + interval) assert server.get_device_data(state) is fresh read.assert_called_once_with(state, 0) def test_offline_bluetooth_does_not_retry_forever_or_serve_stale_data(monkeypatch): state = server.DeviceState( data=JBDBMS(), timestamp=1, ecoflow=initialise_device(ADDRESS, SERIAL, "test-user"), ) read = Mock(return_value=None) monkeypatch.setattr(server, "collect_device_data", read) monkeypatch.setattr(server.time, "time", lambda: 100) assert server.get_device_data(state) is None assert state.timestamp == 100 # A failed poll also has a backoff, rather than reconnecting on every GET. assert server.get_device_data(state) is None read.assert_called_once_with(state, 0) monkeypatch.setattr(server.time, "time", lambda: 160) assert server.get_device_data(state) is None assert read.call_count == 2 def test_serial_collection_still_retries(monkeypatch): data = JBDBMS() state = server.DeviceState(ser=Mock()) read = Mock(side_effect=[None, data]) sleep = Mock() monkeypatch.setattr(server, "collect_device_data", read) monkeypatch.setattr(server.time, "time", lambda: 100) monkeypatch.setattr(server.time, "sleep", sleep) assert server.get_device_data(state) is data assert read.call_count == 2 sleep.assert_called_once_with(1)