diff --git a/src/meshcore/channel_sync.py b/src/meshcore/channel_sync.py index 654e508..a3f3a1a 100644 --- a/src/meshcore/channel_sync.py +++ b/src/meshcore/channel_sync.py @@ -5,7 +5,11 @@ import logging from typing import TYPE_CHECKING, Optional -from src.meshcore.channels import read_device_channels, snapshot_sync_body +from src.meshcore.channels import ( + log_device_channels, + read_device_channels, + snapshot_sync_body, +) if TYPE_CHECKING: from src.api.StorageAPI import StorageAPIWrapper @@ -32,8 +36,20 @@ async def sync_channels_to_api_async( logger.exception("MeshCore read_device_channels failed: %s", exc) return False + log_device_channels(channels) body = snapshot_sync_body(channels) - return storage.post_mc_channel_sync(body) + ok = storage.post_mc_channel_sync(body) + if ok: + logger.info( + "MeshCore channel sync posted to API (%s channel(s))", + len(channels), + ) + else: + logger.warning( + "MeshCore channel sync to API failed (%s channel(s) read from device)", + len(channels), + ) + return ok def sync_channels_to_api(radio: "MeshCoreRadio", storage: "StorageAPIWrapper") -> bool: diff --git a/src/meshcore/channels.py b/src/meshcore/channels.py index 3012f25..c430637 100644 --- a/src/meshcore/channels.py +++ b/src/meshcore/channels.py @@ -67,6 +67,29 @@ async def apply_device_channels(meshcore: MeshCore, channels: list[dict]) -> Non logger.warning("set_channel(%s) failed: %s", idx, evt.payload) +def log_device_channels(channels: list[dict]) -> None: + """Log the device channel table at INFO (visible in docker logs on connect).""" + if not channels: + logger.info("MeshCore device channels: (none configured on device)") + return + logger.info("MeshCore device channels (%s):", len(channels)) + for ch in sorted(channels, key=lambda c: int(c["mc_channel_idx"])): + idx = ch["mc_channel_idx"] + typ = ch.get("mc_channel_type", "?") + name = ch.get("name", "") + tag = ch.get("mc_hashtag") + if tag: + logger.info( + " [%s] %s name=%r hashtag=%r", + idx, + typ, + name, + tag, + ) + else: + logger.info(" [%s] %s name=%r", idx, typ, name) + + def snapshot_sync_body(channels: list[dict]) -> dict: return { "channels": channels, diff --git a/src/meshcore/radio.py b/src/meshcore/radio.py index dfec07d..22c0736 100644 --- a/src/meshcore/radio.py +++ b/src/meshcore/radio.py @@ -290,8 +290,10 @@ def schedule_channel_sync(self, storage_apis: list) -> None: async def _task() -> None: from src.meshcore.channel_sync import sync_channels_to_api_async + logger.info("MeshCore channel sync starting") for storage in storage_apis: await sync_channels_to_api_async(self, storage) + logger.info("MeshCore channel sync finished") asyncio.create_task(_task()) diff --git a/test/meshcore/test_channel_sync.py b/test/meshcore/test_channel_sync.py index da5f197..d50501a 100644 --- a/test/meshcore/test_channel_sync.py +++ b/test/meshcore/test_channel_sync.py @@ -50,6 +50,28 @@ async def _run(): storage.post_mc_channel_sync.assert_called_once() +def test_sync_channels_logs_and_reports_api_failure(caplog) -> None: + import logging + + caplog.set_level(logging.INFO) + radio = _MeshCoreRadioStub(connected=True, meshcore=MagicMock()) + storage = MagicMock() + storage.post_mc_channel_sync.return_value = False + channels = [{"mc_channel_idx": 0, "name": "Public", "mc_channel_type": "PUBLIC"}] + + async def _run(): + with patch( + "src.meshcore.channel_sync.read_device_channels", + new_callable=AsyncMock, + return_value=channels, + ): + return await sync_channels_to_api_async(radio, storage) + + assert asyncio.run(_run()) is False + assert "MeshCore device channels (1):" in caplog.text + assert "channel sync to API failed" in caplog.text + + def test_sync_channels_skipped_when_disconnected() -> None: radio = _MeshCoreRadioStub(connected=False) storage = MagicMock() diff --git a/test/meshcore/test_channels.py b/test/meshcore/test_channels.py index 3813e00..c5c7e53 100644 --- a/test/meshcore/test_channels.py +++ b/test/meshcore/test_channels.py @@ -10,6 +10,7 @@ from src.meshcore.channels import ( _channel_entry_from_info, apply_device_channels, + log_device_channels, read_device_channels, snapshot_sync_body, ) @@ -27,6 +28,35 @@ def test_channel_entry_hashtag(): assert entry["mc_hashtag"] == "galloway" +def test_log_device_channels(caplog) -> None: + import logging + + caplog.set_level(logging.INFO) + log_device_channels( + [ + {"mc_channel_idx": 0, "name": "Public", "mc_channel_type": "PUBLIC"}, + { + "mc_channel_idx": 1, + "name": "galloway", + "mc_channel_type": "HASHTAG", + "mc_hashtag": "galloway", + }, + ] + ) + text = caplog.text + assert "MeshCore device channels (2):" in text + assert "Public" in text + assert "galloway" in text + + +def test_log_device_channels_empty(caplog) -> None: + import logging + + caplog.set_level(logging.INFO) + log_device_channels([]) + assert "none configured" in caplog.text + + def test_snapshot_sync_body(): body = snapshot_sync_body( [{"mc_channel_idx": 0, "name": "X", "mc_channel_type": "PUBLIC"}]