mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-06-10 16:34:49 +02:00
Emit UTC Z-suffixed timestamps for Community MQTT observer payloads
This commit is contained in:
@@ -15,7 +15,7 @@ import json
|
||||
import logging
|
||||
import ssl
|
||||
import time
|
||||
from datetime import datetime
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any, Protocol
|
||||
|
||||
import aiomqtt
|
||||
@@ -45,6 +45,12 @@ _STATS_MIN_CACHE_SECS = 60 # Don't re-fetch stats within 60s
|
||||
_ROUTE_MAP = {0: "F", 1: "F", 2: "D", 3: "T"}
|
||||
|
||||
|
||||
def _format_utc_timestamp(dt: datetime | None = None) -> str:
|
||||
"""Return an ISO-8601 UTC timestamp accepted by community observers."""
|
||||
current = dt.astimezone(UTC) if dt is not None else datetime.now(UTC)
|
||||
return current.isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
class CommunityMqttSettings(Protocol):
|
||||
"""Attributes expected on the settings object for the community MQTT publisher."""
|
||||
|
||||
@@ -152,9 +158,9 @@ def _format_raw_packet(data: dict[str, Any], device_name: str, public_key_hex: s
|
||||
if route == "U":
|
||||
return None
|
||||
|
||||
# Reference format uses local "now" timestamp and derived time/date fields.
|
||||
current_time = datetime.now()
|
||||
ts_str = current_time.isoformat()
|
||||
# Community observers clamp zone-less local timestamps; publish explicit UTC.
|
||||
current_time = datetime.now(UTC)
|
||||
ts_str = _format_utc_timestamp(current_time)
|
||||
|
||||
# Keep numeric telemetry numeric so downstream analyzers can ingest it.
|
||||
# Preserve the existing "Unknown" fallback for missing values.
|
||||
@@ -314,7 +320,7 @@ class CommunityMqttPublisher(BaseMqttPublisher):
|
||||
offline_payload = json.dumps(
|
||||
{
|
||||
"status": "offline",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"timestamp": _format_utc_timestamp(),
|
||||
"origin": device_name or "MeshCore Device",
|
||||
"origin_id": pubkey_hex,
|
||||
}
|
||||
@@ -478,7 +484,7 @@ class CommunityMqttPublisher(BaseMqttPublisher):
|
||||
status_topic = _build_status_topic(settings, pubkey_hex)
|
||||
payload: dict[str, Any] = {
|
||||
"status": "online",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"timestamp": _format_utc_timestamp(),
|
||||
"origin": device_name or "MeshCore Device",
|
||||
"origin_id": pubkey_hex,
|
||||
"model": device_info.get("model", "unknown"),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import json
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import UTC, datetime
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
@@ -76,6 +77,13 @@ def _make_community_settings(**overrides) -> SimpleNamespace:
|
||||
return SimpleNamespace(**defaults)
|
||||
|
||||
|
||||
def _assert_utc_z_timestamp(value: str) -> None:
|
||||
assert value.endswith("Z")
|
||||
assert "+00:00" not in value
|
||||
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
assert parsed.tzinfo == UTC
|
||||
|
||||
|
||||
class TestBase64UrlEncode:
|
||||
def test_encodes_without_padding(self):
|
||||
result = _base64url_encode(b"\x00\x01\x02")
|
||||
@@ -219,12 +227,11 @@ class TestPacketFormatConversion:
|
||||
assert result["direction"] == "rx"
|
||||
assert result["len"] == "3"
|
||||
|
||||
def test_timestamp_is_iso8601(self):
|
||||
def test_timestamp_is_utc_z_iso8601(self):
|
||||
data = {"timestamp": 1700000000, "data": "0100AA", "snr": None, "rssi": None}
|
||||
result = _format_raw_packet(data, "Node", "AA" * 32)
|
||||
assert result is not None
|
||||
assert result["timestamp"]
|
||||
assert "T" in result["timestamp"]
|
||||
_assert_utc_z_timestamp(result["timestamp"])
|
||||
|
||||
def test_snr_rssi_unknown_when_none(self):
|
||||
data = {"timestamp": 0, "data": "0100AA", "snr": None, "rssi": None}
|
||||
@@ -734,7 +741,7 @@ class TestLwtAndStatusPublish:
|
||||
assert payload["status"] == "offline"
|
||||
assert payload["origin"] == "TestNode"
|
||||
assert payload["origin_id"] == pubkey_hex
|
||||
assert "timestamp" in payload
|
||||
_assert_utc_z_timestamp(payload["timestamp"])
|
||||
assert "client" not in payload
|
||||
assert kwargs["transport"] == "websockets"
|
||||
assert kwargs["websocket_path"] == "/"
|
||||
@@ -884,7 +891,7 @@ class TestLwtAndStatusPublish:
|
||||
assert payload["origin"] == "TestNode"
|
||||
assert payload["origin_id"] == pubkey_hex
|
||||
assert "client" not in payload
|
||||
assert "timestamp" in payload
|
||||
_assert_utc_z_timestamp(payload["timestamp"])
|
||||
assert payload["model"] == "T-Deck"
|
||||
assert payload["firmware_version"] == "v2.2.2 (Build: 2025-01-15)"
|
||||
assert payload["radio"] == "915.0,250.0,10,8"
|
||||
@@ -1393,6 +1400,7 @@ class TestPublishStatus:
|
||||
assert payload["origin"] == "TestNode"
|
||||
assert payload["origin_id"] == pubkey_hex
|
||||
assert "client" not in payload
|
||||
_assert_utc_z_timestamp(payload["timestamp"])
|
||||
assert payload["model"] == "T-Deck"
|
||||
assert payload["firmware_version"] == "v2.2.2 (Build: 2025-01-15)"
|
||||
assert payload["radio"] == "915.0,250.0,10,8"
|
||||
|
||||
Reference in New Issue
Block a user