Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/meshcore/channel_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
23 changes: 23 additions & 0 deletions src/meshcore/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/meshcore/radio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down
22 changes: 22 additions & 0 deletions test/meshcore/test_channel_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
30 changes: 30 additions & 0 deletions test/meshcore/test_channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand All @@ -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"}]
Expand Down