|
12 | 12 | import datetime |
13 | 13 | import json |
14 | 14 | import logging |
| 15 | +import math |
15 | 16 | from collections import OrderedDict |
16 | 17 | from pathlib import Path |
17 | 18 | from typing import Any, Dict, Optional, List, Tuple |
@@ -41,6 +42,24 @@ def _json_default(obj: Any) -> Any: |
41 | 42 | return str(obj) |
42 | 43 |
|
43 | 44 |
|
| 45 | +def _finite_or_none(value: Any) -> Any: |
| 46 | + """Map non-finite telemetry metrics to None for typed numeric columns. |
| 47 | +
|
| 48 | + Protobuf's proto3 JSON mapping serializes non-finite floats as the |
| 49 | + strings "NaN"/"Infinity"/"-Infinity" (and some decode paths yield real |
| 50 | + non-finite floats); asyncpg can bind neither into a numeric column, so |
| 51 | + the whole node write fails. Everything else passes through unchanged.""" |
| 52 | + if isinstance(value, float) and not math.isfinite(value): |
| 53 | + return None |
| 54 | + if isinstance(value, str): |
| 55 | + try: |
| 56 | + if not math.isfinite(float(value)): |
| 57 | + return None |
| 58 | + except (TypeError, ValueError): |
| 59 | + pass |
| 60 | + return value |
| 61 | + |
| 62 | + |
44 | 63 | def _encode_cursor(created_at: datetime.datetime, row_id: int) -> str: |
45 | 64 | """Opaque keyset-pagination cursor for mqtt_messages ordered by (created_at, id) DESC.""" |
46 | 65 | raw = f"{created_at.isoformat()}|{row_id}".encode() |
@@ -830,7 +849,7 @@ async def _write_node_telemetry_current(self, conn, node_id: str, telemetry: Dic |
830 | 849 | present: Dict[str, Any] = {} |
831 | 850 | for payload_key, col_name in self.TELEMETRY_COLUMNS.items(): |
832 | 851 | if payload_key in telemetry: |
833 | | - present[col_name] = telemetry[payload_key] |
| 852 | + present[col_name] = _finite_or_none(telemetry[payload_key]) |
834 | 853 |
|
835 | 854 | if not present: |
836 | 855 | logger.debug("_write_node_telemetry_current: no recognized telemetry fields for node %s; skipping", node_norm) |
|
0 commit comments