Add radio model and stats display. Closes #64

This commit is contained in:
Jack Kingsman
2026-03-16 15:29:21 -07:00
parent 58b34a6a2f
commit ea5ba3b2a3
43 changed files with 431 additions and 116 deletions
+30
View File
@@ -78,6 +78,11 @@ class TestHealthEndpoint:
with patch("app.routers.health.radio_manager") as mock_rm:
mock_rm.is_connected = True
mock_rm.connection_info = "Serial: /dev/ttyUSB0"
mock_rm.is_setup_in_progress = False
mock_rm.is_setup_complete = True
mock_rm.connection_desired = True
mock_rm.is_reconnecting = False
mock_rm.device_info_loaded = False
from app.main import app
@@ -97,6 +102,11 @@ class TestHealthEndpoint:
with patch("app.routers.health.radio_manager") as mock_rm:
mock_rm.is_connected = False
mock_rm.connection_info = None
mock_rm.is_setup_in_progress = False
mock_rm.is_setup_complete = False
mock_rm.connection_desired = True
mock_rm.is_reconnecting = False
mock_rm.device_info_loaded = False
from app.main import app
@@ -1118,6 +1128,11 @@ class TestHealthEndpointDatabaseSize:
):
mock_rm.is_connected = True
mock_rm.connection_info = "Serial: /dev/ttyUSB0"
mock_rm.is_setup_in_progress = False
mock_rm.is_setup_complete = True
mock_rm.connection_desired = True
mock_rm.is_reconnecting = False
mock_rm.device_info_loaded = False
mock_getsize.return_value = 10 * 1024 * 1024 # 10 MB
from app.main import app
@@ -1148,6 +1163,11 @@ class TestHealthEndpointOldestUndecrypted:
):
mock_rm.is_connected = True
mock_rm.connection_info = "Serial: /dev/ttyUSB0"
mock_rm.is_setup_in_progress = False
mock_rm.is_setup_complete = True
mock_rm.connection_desired = True
mock_rm.is_reconnecting = False
mock_rm.device_info_loaded = False
mock_getsize.return_value = 5 * 1024 * 1024 # 5 MB
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=1700000000)
@@ -1175,6 +1195,11 @@ class TestHealthEndpointOldestUndecrypted:
):
mock_rm.is_connected = True
mock_rm.connection_info = "Serial: /dev/ttyUSB0"
mock_rm.is_setup_in_progress = False
mock_rm.is_setup_complete = True
mock_rm.connection_desired = True
mock_rm.is_reconnecting = False
mock_rm.device_info_loaded = False
mock_getsize.return_value = 1 * 1024 * 1024 # 1 MB
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=None)
@@ -1202,6 +1227,11 @@ class TestHealthEndpointOldestUndecrypted:
):
mock_rm.is_connected = False
mock_rm.connection_info = None
mock_rm.is_setup_in_progress = False
mock_rm.is_setup_complete = False
mock_rm.connection_desired = True
mock_rm.is_reconnecting = False
mock_rm.device_info_loaded = False
mock_getsize.side_effect = OSError("File not found")
mock_repo.get_oldest_undecrypted = AsyncMock(side_effect=RuntimeError("No DB"))
+29
View File
@@ -59,6 +59,35 @@ class TestHealthFanoutStatus:
assert data["radio_state"] == "connected"
assert data["connection_info"] == "Serial: /dev/ttyUSB0"
@pytest.mark.asyncio
async def test_health_includes_cached_radio_device_info(self, test_db):
"""Health includes device metadata captured during post-connect setup."""
with (
patch(
"app.routers.health.RawPacketRepository.get_oldest_undecrypted", return_value=None
),
patch("app.routers.health.radio_manager") as mock_rm,
):
mock_rm.is_setup_in_progress = False
mock_rm.is_setup_complete = True
mock_rm.connection_desired = True
mock_rm.is_reconnecting = False
mock_rm.device_info_loaded = True
mock_rm.device_model = "T-Echo"
mock_rm.firmware_build = "2025-02-01"
mock_rm.firmware_version = "1.2.3"
mock_rm.max_contacts = 350
mock_rm.max_channels = 64
data = await build_health_data(True, "Serial: /dev/ttyUSB0")
assert data["radio_device_info"] == {
"model": "T-Echo",
"firmware_build": "2025-02-01",
"firmware_version": "1.2.3",
"max_contacts": 350,
"max_channels": 64,
}
@pytest.mark.asyncio
async def test_health_status_degraded_when_disconnected(self, test_db):
"""Health status is 'degraded' when radio is disconnected."""
+10
View File
@@ -476,6 +476,11 @@ class TestManualDisconnectCleanup:
mock_mc.connection_manager = connection_manager
rm._meshcore = mock_mc
rm._setup_complete = True
rm.device_info_loaded = True
rm.max_contacts = 350
rm.device_model = "T-Echo"
rm.firmware_build = "2025-02-01"
rm.firmware_version = "1.2.3"
rm.max_channels = 8
rm.path_hash_mode = 2
rm.path_hash_mode_supported = True
@@ -491,6 +496,11 @@ class TestManualDisconnectCleanup:
assert reconnect_task is not None and reconnect_task.cancelled()
assert rm.meshcore is None
assert rm.is_setup_complete is False
assert rm.device_info_loaded is False
assert rm.max_contacts is None
assert rm.device_model is None
assert rm.firmware_build is None
assert rm.firmware_version is None
assert rm.max_channels == 40
assert rm.path_hash_mode == 0
assert rm.path_hash_mode_supported is False
+68
View File
@@ -112,6 +112,11 @@ class TestRunPostConnectSetup:
radio_manager._setup_lock = None
radio_manager._setup_in_progress = False
radio_manager._setup_complete = False
radio_manager.device_info_loaded = False
radio_manager.max_contacts = None
radio_manager.device_model = None
radio_manager.firmware_build = None
radio_manager.firmware_version = None
radio_manager.max_channels = 40
radio_manager.path_hash_mode = 0
radio_manager.path_hash_mode_supported = False
@@ -145,3 +150,66 @@ class TestRunPostConnectSetup:
replacement_mc.start_auto_message_fetching.assert_awaited_once()
initial_mc.start_auto_message_fetching.assert_not_called()
assert radio_manager.max_channels == 8
@pytest.mark.asyncio
async def test_caches_device_info_metadata_from_device_query(self):
mc = MagicMock()
mc.commands.send_device_query = AsyncMock(
return_value=MagicMock(
payload={
"fw ver": 10,
"max_contacts": 350,
"max_channels": 64,
"model": "T-Echo",
"fw_build": "2025-02-01",
"ver": "1.2.3",
"path_hash_mode": 2,
}
)
)
mc.commands.set_flood_scope = AsyncMock(return_value=None)
mc._reader = MagicMock()
mc._reader.handle_rx = AsyncMock()
mc.start_auto_message_fetching = AsyncMock()
radio_manager = MagicMock()
radio_manager.meshcore = mc
radio_manager._setup_lock = None
radio_manager._setup_in_progress = False
radio_manager._setup_complete = False
radio_manager.device_info_loaded = False
radio_manager.max_contacts = None
radio_manager.device_model = None
radio_manager.firmware_build = None
radio_manager.firmware_version = None
radio_manager.max_channels = 40
radio_manager.path_hash_mode = 0
radio_manager.path_hash_mode_supported = False
radio_manager._acquire_operation_lock = AsyncMock()
radio_manager._release_operation_lock = MagicMock()
with (
patch("app.event_handlers.register_event_handlers"),
patch("app.keystore.export_and_store_private_key", new=AsyncMock()),
patch("app.radio_sync.sync_radio_time", new=AsyncMock()),
patch(
"app.repository.AppSettingsRepository.get",
new=AsyncMock(return_value=MagicMock(flood_scope=None)),
),
patch("app.radio_sync.sync_and_offload_all", new=AsyncMock(return_value={"synced": 0})),
patch("app.radio_sync.send_advertisement", new=AsyncMock(return_value=False)),
patch("app.radio_sync.drain_pending_messages", new=AsyncMock(return_value=0)),
patch("app.radio_sync.start_periodic_sync"),
patch("app.radio_sync.start_periodic_advert"),
patch("app.radio_sync.start_message_polling"),
):
await run_post_connect_setup(radio_manager)
assert radio_manager.device_info_loaded is True
assert radio_manager.max_contacts == 350
assert radio_manager.max_channels == 64
assert radio_manager.device_model == "T-Echo"
assert radio_manager.firmware_build == "2025-02-01"
assert radio_manager.firmware_version == "1.2.3"
assert radio_manager.path_hash_mode == 2
assert radio_manager.path_hash_mode_supported is True
+25
View File
@@ -41,6 +41,11 @@ class TestWebSocketEndpoint:
mock_ws_rm.connection_info = "Serial: /dev/ttyUSB0"
mock_health_rm.is_connected = True
mock_health_rm.connection_info = "Serial: /dev/ttyUSB0"
mock_health_rm.is_setup_in_progress = False
mock_health_rm.is_setup_complete = True
mock_health_rm.connection_desired = True
mock_health_rm.is_reconnecting = False
mock_health_rm.device_info_loaded = False
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=None)
mock_settings.database_path = "/tmp/test.db"
mock_settings.disable_bots = False
@@ -75,6 +80,11 @@ class TestWebSocketEndpoint:
mock_ws_rm.connection_info = None
mock_health_rm.is_connected = False
mock_health_rm.connection_info = None
mock_health_rm.is_setup_in_progress = False
mock_health_rm.is_setup_complete = False
mock_health_rm.connection_desired = True
mock_health_rm.is_reconnecting = False
mock_health_rm.device_info_loaded = False
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=None)
mock_settings.database_path = "/tmp/test.db"
mock_settings.disable_bots = False
@@ -105,6 +115,11 @@ class TestWebSocketEndpoint:
mock_ws_rm.connection_info = "TCP: 192.168.1.1:4000"
mock_health_rm.is_connected = True
mock_health_rm.connection_info = "TCP: 192.168.1.1:4000"
mock_health_rm.is_setup_in_progress = False
mock_health_rm.is_setup_complete = True
mock_health_rm.connection_desired = True
mock_health_rm.is_reconnecting = False
mock_health_rm.device_info_loaded = False
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=None)
mock_settings.database_path = "/tmp/test.db"
mock_settings.disable_bots = False
@@ -136,6 +151,11 @@ class TestWebSocketEndpoint:
mock_ws_rm.connection_info = "Serial: /dev/ttyUSB0"
mock_health_rm.is_connected = True
mock_health_rm.connection_info = "Serial: /dev/ttyUSB0"
mock_health_rm.is_setup_in_progress = False
mock_health_rm.is_setup_complete = True
mock_health_rm.connection_desired = True
mock_health_rm.is_reconnecting = False
mock_health_rm.device_info_loaded = False
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=None)
mock_settings.database_path = "/tmp/test.db"
mock_settings.disable_bots = False
@@ -167,6 +187,11 @@ class TestWebSocketEndpoint:
mock_ws_rm.connection_info = "Serial: /dev/ttyUSB0"
mock_health_rm.is_connected = True
mock_health_rm.connection_info = "Serial: /dev/ttyUSB0"
mock_health_rm.is_setup_in_progress = False
mock_health_rm.is_setup_complete = True
mock_health_rm.connection_desired = True
mock_health_rm.is_reconnecting = False
mock_health_rm.device_info_loaded = False
mock_repo.get_oldest_undecrypted = AsyncMock(return_value=None)
mock_settings.database_path = "/tmp/test.db"
mock_settings.disable_bots = False