Add repeater telemetry error count

This commit is contained in:
Jack Kingsman
2026-04-22 17:35:25 -07:00
parent f0b7842c60
commit b37ce89c96
13 changed files with 34 additions and 2 deletions

View File

@@ -81,6 +81,15 @@ _REPEATER_SENSORS: list[dict[str, Any]] = [
"unit": None,
"precision": 0,
},
{
"field": "recv_errors",
"name": "RX Errors",
"object_id": "recv_errors",
"device_class": None,
"state_class": "total_increasing",
"unit": None,
"precision": 0,
},
{
"field": "uptime_seconds",
"name": "Uptime",

View File

@@ -540,6 +540,7 @@ class RepeaterStatusResponse(BaseModel):
flood_dups: int = Field(description="Duplicate flood packets")
direct_dups: int = Field(description="Duplicate direct packets")
full_events: int = Field(description="Full event queue count")
recv_errors: int | None = Field(default=None, description="Radio-level RX packet errors")
telemetry_history: list["TelemetryHistoryEntry"] = Field(
default_factory=list, description="Recent telemetry history snapshots"
)

View File

@@ -1821,6 +1821,7 @@ async def _collect_repeater_telemetry(mc: MeshCore, contact: Contact) -> bool:
"flood_dups": status.get("flood_dups", 0),
"direct_dups": status.get("direct_dups", 0),
"full_events": status.get("full_evts", 0),
"recv_errors": status.get("recv_errors"),
}
# Best-effort LPP sensor fetch — failure here does not fail the overall

View File

@@ -133,6 +133,7 @@ async def repeater_status(public_key: str) -> RepeaterStatusResponse:
flood_dups=status.get("flood_dups", 0),
direct_dups=status.get("direct_dups", 0),
full_events=status.get("full_evts", 0),
recv_errors=status.get("recv_errors"),
)
# Record to telemetry history as a JSON blob (best-effort)

View File

@@ -78,6 +78,7 @@ async def room_status(public_key: str) -> RepeaterStatusResponse:
flood_dups=status.get("flood_dups", 0),
direct_dups=status.get("direct_dups", 0),
full_events=status.get("full_evts", 0),
recv_errors=status.get("recv_errors"),
)

View File

@@ -17,7 +17,12 @@ import type { TelemetryHistoryEntry, TelemetryLppSensor, Contact } from '../../t
const MAX_TRACKED = 8;
type BuiltinMetric = 'battery_volts' | 'noise_floor_dbm' | 'packets' | 'uptime_seconds';
type BuiltinMetric =
| 'battery_volts'
| 'noise_floor_dbm'
| 'packets'
| 'recv_errors'
| 'uptime_seconds';
interface MetricConfig {
label: string;
@@ -29,6 +34,7 @@ const BUILTIN_METRIC_CONFIG: Record<BuiltinMetric, MetricConfig> = {
battery_volts: { label: 'Voltage', unit: 'V', color: '#22c55e' },
noise_floor_dbm: { label: 'Noise Floor', unit: 'dBm', color: '#8b5cf6' },
packets: { label: 'Packets', unit: '', color: '#0ea5e9' },
recv_errors: { label: 'RX Errors', unit: '', color: '#ef4444' },
uptime_seconds: { label: 'Uptime', unit: 's', color: '#f59e0b' },
};
@@ -154,6 +160,7 @@ export function TelemetryHistoryPane({
noise_floor_dbm: d.noise_floor_dbm,
packets_received: d.packets_received,
packets_sent: d.packets_sent,
recv_errors: d.recv_errors ?? undefined,
uptime_seconds: d.uptime_seconds,
};
// Flatten LPP sensors into the point, converting units as needed

View File

@@ -91,6 +91,9 @@ export function TelemetryPane({
label="Duplicates"
value={`${data.flood_dups.toLocaleString()} flood / ${data.direct_dups.toLocaleString()} direct`}
/>
{data.recv_errors != null && (
<KvRow label="RX Errors" value={data.recv_errors.toLocaleString()} />
)}
<Separator className="my-1" />
<KvRow label="TX Queue" value={data.tx_queue_len} />
<KvRow label="Debug Flags" value={data.full_events} />

View File

@@ -438,6 +438,7 @@ describe('RepeaterDashboard', () => {
flood_dups: 1,
direct_dups: 0,
full_events: 0,
recv_errors: 5,
telemetry_history: [],
};
@@ -707,6 +708,7 @@ describe('RepeaterDashboard', () => {
flood_dups: 1,
direct_dups: 0,
full_events: 0,
recv_errors: null,
telemetry_history: [liveEntry],
};
@@ -742,6 +744,7 @@ describe('RepeaterDashboard', () => {
flood_dups: 1,
direct_dups: 0,
full_events: 0,
recv_errors: null,
telemetry_history: [{ timestamp: 1700000000, data: { battery_volts: 4.2 } }],
};

View File

@@ -438,6 +438,7 @@ export interface RepeaterStatusResponse {
flood_dups: number;
direct_dups: number;
full_events: number;
recv_errors: number | null;
telemetry_history: TelemetryHistoryEntry[];
}

View File

@@ -125,7 +125,7 @@ class TestRadioDiscovery:
class TestRepeaterDiscovery:
def test_produces_sensor_per_field(self):
configs = _repeater_discovery_configs("mc", "ccdd11223344", "Rep1", "aabb")
assert len(configs) == 7 # matches _REPEATER_SENSORS length
assert len(configs) == 8 # matches _REPEATER_SENSORS length
topics = [t for t, _ in configs]
assert "homeassistant/sensor/meshcore_ccdd11223344/battery_voltage/config" in topics

View File

@@ -722,6 +722,7 @@ class TestRepeaterStatus:
"flood_dups": 10,
"direct_dups": 5,
"full_evts": 0,
"recv_errors": 42,
}
)
@@ -741,6 +742,7 @@ class TestRepeaterStatus:
assert response.uptime_seconds == 86400
assert response.sent_flood == 100
assert response.recv_direct == 700
assert response.recv_errors == 42
@pytest.mark.asyncio
async def test_504_on_timeout(self, test_db):

View File

@@ -31,6 +31,7 @@ SAMPLE_STATUS = {
"flood_dups": 5,
"direct_dups": 2,
"full_events": 0,
"recv_errors": None,
}

View File

@@ -135,6 +135,7 @@ class TestRoomStatus:
"flood_dups": 2,
"direct_dups": 1,
"full_evts": 0,
"recv_errors": 7,
}
)
@@ -147,6 +148,7 @@ class TestRoomStatus:
assert response.battery_volts == 4.025
assert response.packets_received == 80
assert response.recv_direct == 73
assert response.recv_errors == 7
@pytest.mark.asyncio
async def test_room_acl_maps_entries(self, test_db):