Merge pull request #280 from Bjorkan/TimezoneFix

Emit UTC Z-suffixed timestamps for Community MQTT observer payloads
This commit is contained in:
Jack Kingsman
2026-06-05 20:54:25 -07:00
committed by GitHub
2 changed files with 25 additions and 11 deletions
+12 -6
View File
@@ -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"),
+13 -5
View File
@@ -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"