This commit is contained in:
Lars Nolden
2026-07-24 13:57:13 +02:00
parent fc8813c14a
commit 50a9ee8c58
16 changed files with 206 additions and 32 deletions
+3 -1
View File
@@ -88,7 +88,9 @@ Keys:
| `q` | quit |
Details include manufacturer, media/type, driver, version, RSSI, first/last
seen, telegram count, the encryption in use (OMS security mode 5 = AES-CBC,
seen, telegram count, the estimated **send interval** (median of the gaps
between telegrams, so missed transmissions don't skew it — needs ≥2 telegrams),
the encryption in use (OMS security mode 5 = AES-CBC,
mode 7 = AES-CTR, or ELL AES-CTR), TPL/ELL layer info, and — once decoded — the
latest measurement values with a timestamp. Unencrypted meters show as **open**
(📖, readable without a key); encrypted ones are **locked** (🔒) until you add a
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+27
View File
@@ -18,6 +18,7 @@ from rich.console import Group
from rich.table import Table
from rich.text import Text
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Horizontal, Vertical, VerticalScroll
from textual.message import Message
from textual.screen import ModalScreen
@@ -41,6 +42,19 @@ def rel_time(ts: float) -> str:
return f"{d // 3600}h {(d % 3600) // 60}m ago"
def fmt_duration(seconds: Optional[float]) -> str:
if seconds is None:
return "?"
s = int(round(seconds))
if s < 60:
return f"{s}s"
if s < 3600:
return f"{s // 60}m {s % 60:02d}s"
if s < 86400:
return f"{s // 3600}h {(s % 3600) // 60:02d}m"
return f"{s // 86400}d {(s % 86400) // 3600:02d}h"
def stream_text(ts_str: str, ev: dict) -> Text:
if ev.get("type") == "json":
fields = ev.get("fields", {})
@@ -109,6 +123,8 @@ class OmsApp(App[None]):
("a", "show_all", "Show all / stream"),
("enter", "select", "Select device"),
("q", "quit", "Quit"),
# priority so Ctrl+C quits even while a modal Input is focused
Binding("ctrl+c", "quit", "Quit", priority=True, show=False),
]
COLUMNS = [
@@ -337,6 +353,17 @@ class OmsApp(App[None]):
meta.add_row("link (C)", dev.c_field or "?")
meta.add_row("rssi", "?" if dev.rssi is None else f"{dev.rssi} dBm")
meta.add_row("telegrams", str(dev.count))
if dev.interval_samples >= 1:
est = fmt_duration(dev.interval_estimate)
last = fmt_duration(dev.interval_last)
detail = f"~{est}"
extra = f"last {last}, {dev.interval_samples} sample"
extra += "s" if dev.interval_samples != 1 else ""
interval_text = Text(detail, style="bold")
interval_text.append(f" ({extra})", style="dim")
meta.add_row("send interval", interval_text)
else:
meta.add_row("send interval", Text("— (need ≥2 telegrams)", style="dim"))
meta.add_row(
"first seen", time.strftime("%H:%M:%S", time.localtime(dev.first_seen))
)
+7
View File
@@ -19,6 +19,8 @@ FIELDNAMES = [
"last_seen",
"count",
"rssi",
"interval_s",
"interval_samples",
"encryption",
"security_mode",
"status",
@@ -50,6 +52,11 @@ def export_devices(path: str, devices: Iterable[Device]) -> int:
"last_seen": _fmt_time(d.last_seen),
"count": d.count,
"rssi": "" if d.rssi is None else d.rssi,
"interval_s": (
"" if d.interval_estimate is None
else round(d.interval_estimate, 1)
),
"interval_samples": d.interval_samples,
"encryption": d.encryption,
"security_mode": d.sec_mode or "",
"status": d.status,
+51 -5
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import statistics
import time
from collections import deque
from dataclasses import dataclass, field
@@ -42,6 +43,37 @@ class Device:
default_factory=lambda: deque(maxlen=500)
)
_last_count_at: float = 0.0 # for de-duplicating multi-meter DLL bursts
# arrival times of counted telegrams, for estimating the send interval
rx_times: Deque[float] = field(default_factory=lambda: deque(maxlen=64))
@property
def intervals(self) -> List[float]:
t = list(self.rx_times)
return [b - a for a, b in zip(t, t[1:])]
@property
def interval_samples(self) -> int:
return max(0, len(self.rx_times) - 1)
@property
def interval_estimate(self) -> Optional[float]:
"""Best estimate of the transmit period: median of observed gaps.
Median is robust to missed telegrams (which show up as gaps that are
multiples of the true period) and to the odd retransmission burst.
"""
gaps = self.intervals
return statistics.median(gaps) if gaps else None
@property
def interval_last(self) -> Optional[float]:
gaps = self.intervals
return gaps[-1] if gaps else None
@property
def interval_min(self) -> Optional[float]:
gaps = self.intervals
return min(gaps) if gaps else None
@property
def encryption(self) -> str:
@@ -113,6 +145,20 @@ class DeviceStore:
def ordered(self) -> List[Device]:
return [self.devices[i] for i in self.order]
@staticmethod
def _note_telegram(dev: Device, now: float) -> None:
"""Count one telegram and record its arrival, de-duplicating the
multi-line burst a single telegram produces (DLL[+DLL]+ELL+TPL+JSON,
all within microseconds) so counts and the interval stay accurate.
Driven by *any* telegram-level event, so a DLL line we fail to parse
can't silently zero out the count and send-interval.
"""
if dev._last_count_at == 0.0 or (now - dev._last_count_at) > COUNT_DEDUP_WINDOW:
dev.count += 1
dev.rx_times.append(now)
dev._last_count_at = now
def _get_or_create(self, device_id: str, now: float) -> Tuple[Device, bool]:
dev = self.devices.get(device_id)
if dev is None:
@@ -134,11 +180,7 @@ class DeviceStore:
if ev.get("rssi") is not None:
dev.rssi = ev["rssi"]
dev.last_seen = now
# Count once per telegram, ignoring the duplicate DLL burst from a
# second matching meter.
if is_new or (now - dev._last_count_at) > COUNT_DEDUP_WINDOW:
dev.count += 1
dev._last_count_at = now
self._note_telegram(dev, now)
return dev, is_new
def apply_ell(self, ev: dict, device_id: Optional[str]) -> Optional[Device]:
@@ -192,4 +234,8 @@ class DeviceStore:
if from_key:
dev.key_decrypted = True
dev.last_seen = now
# Also drive the counter from JSON, so telegrams still count even if
# their DLL line failed to parse. The dedup window collapses the DLL
# and its JSON (same telegram) into one.
self._note_telegram(dev, now)
return dev, is_new
+3 -1
View File
@@ -27,7 +27,9 @@ _DLL_RE = re.compile(
r"M=(?P<mhex>[0-9a-fA-F]+) \((?P<mfct>[A-Z?]+)\) "
r"A=(?P<id>\w+) "
r"VER=(?P<ver>[0-9a-fA-F]+) "
r"TYPE=(?P<type>[0-9a-fA-F]+) \((?P<media>[^)]*)\) "
# media can itself contain parentheses, e.g. "Radio converter (meter side)";
# match lazily up to the " (driver ...)" that always follows.
r"TYPE=(?P<type>[0-9a-fA-F]+) \((?P<media>.*?)\) "
r"\(driver (?P<driver>\w+)\)"
r"(?:.*?RSSI=(?P<rssi>-?\d+))?"
)
+9
View File
@@ -48,6 +48,15 @@ async def test_device_appears_and_selects():
assert app.selected_id == "76348799"
@pytest.mark.asyncio
async def test_ctrl_c_quits():
app = OmsApp(device="none", autostart=False)
async with app.run_test() as pilot:
await pilot.press("ctrl+c")
await pilot.pause()
assert app._exit is True
@pytest.mark.asyncio
async def test_set_key_via_modal():
app = OmsApp(device="none", autostart=False)
+57
View File
@@ -74,6 +74,63 @@ def test_keyed_decrypt_marks_decrypted():
assert dev.status == "decrypted"
def test_send_interval_estimate():
store = DeviceStore()
# telegrams at t=0,16,32,64 -> gaps 16,16,32 (one missed) -> median 16
for t in (1000.0, 1016.0, 1032.0, 1064.0):
store.upsert_discovery(_dll("777"), now=t)
dev = store.get("777")
assert dev.interval_samples == 3
assert dev.interval_estimate == 16
assert dev.interval_last == 32
assert dev.interval_min == 16
def test_json_only_still_counts_and_intervals():
# Regression: a device whose DLL line fails to parse still gets counted
# (and its interval inferred) from the JSON telegrams alone.
store = DeviceStore()
for t in (2000.0, 2016.0, 2032.0):
store.apply_json(
{"type": "json", "id": "37287397", "name": "scan", "driver": "qheatv2",
"media": "radio converter (meter side)", "fields": {"total_kwh": 0}},
now=t,
)
dev = store.get("37287397")
assert dev.count == 3
assert dev.interval_samples == 2
assert dev.interval_estimate == 16
def test_dll_and_its_json_count_once():
# The DLL and its own JSON (same telegram, ~same instant) must not double.
store = DeviceStore()
store.upsert_discovery(_dll("123"), now=3000.0)
store.apply_json(
{"type": "json", "id": "123", "name": "scan", "fields": {"x": 1}},
now=3000.05,
)
assert store.get("123").count == 1
def test_single_telegram_has_no_interval():
store = DeviceStore()
store.upsert_discovery(_dll("888"), now=1000.0)
dev = store.get("888")
assert dev.interval_samples == 0
assert dev.interval_estimate is None
def test_duplicate_burst_does_not_pollute_interval():
store = DeviceStore()
store.upsert_discovery(_dll("999"), now=1000.0)
store.upsert_discovery(_dll("999"), now=1000.002) # duplicate DLL burst
store.upsert_discovery(_dll("999"), now=1016.0)
dev = store.get("999")
assert dev.interval_samples == 1
assert dev.interval_estimate == 16 # not ~0 from the burst
def test_unencrypted_device_is_open_not_locked():
store = DeviceStore()
store.upsert_discovery(_dll("333"))
+16
View File
@@ -33,6 +33,22 @@ def test_parse_json():
assert "rssi_dbm" not in ev["fields"]
def test_parse_dll_with_nested_parens_in_media():
# "Radio converter (meter side)" has parentheses inside the media name.
line = (
"(telegram) DLL L=3c C=44 (from meter SND_NR) M=4493 (QDS) A=37027095 "
"VER=23 TYPE=37 (Radio converter (meter side)) (driver qheatv2) DEV= RSSI=0"
)
ev = parse_line(line)
assert ev is not None
assert ev["type"] == "dll"
assert ev["id"] == "37027095"
assert ev["manufacturer"] == "QDS"
assert ev["media"] == "Radio converter (meter side)"
assert ev["driver"] == "qheatv2"
assert ev["rssi"] == 0
def test_parse_ell_encryption():
line = (
"(telegram) ELL CI=8d CC=20 (slow_resp sync) ACC=91 SN=d37cac21 "
+33 -25
View File
@@ -1,25 +1,33 @@
id,manufacturer,manufacturer_name,media,driver,version,first_seen,last_seen,count,rssi,status,aes_key
23252256,ITW,Itron,Water meter,itron,00,2026-07-24 12:13:32,2026-07-24 12:18:31,2,122,locked,
61379612,,,warm water,qwaterv2,,2026-07-24 12:13:38,2026-07-24 12:19:30,0,,open,
61471660,QDS,Qundis,water,qwaterv2,1d,2026-07-24 12:13:39,2026-07-24 12:19:22,5,124,open,
10179914,ITW,Itron,water,itron,00,2026-07-24 12:14:11,2026-07-24 12:19:11,2,46,open,
23252389,ITW,Itron,Water meter,itron,00,2026-07-24 12:14:38,2026-07-24 12:19:38,2,130,locked,
37287397,,,radio converter (meter side),qheatv2,,2026-07-24 12:14:39,2026-07-24 12:20:15,0,,open,
09237338,ITW,Itron,Cold water meter,itron,00,2026-07-24 12:14:52,2026-07-24 12:19:52,2,61,locked,
68544330,,,heat,qheatv2,,2026-07-24 12:14:56,2026-07-24 12:14:56,0,,open,
23252290,ITW,Itron,Water meter,itron,00,2026-07-24 12:15:16,2026-07-24 12:15:16,1,34,locked,
00258334,ITW,Itron,water,itron,00,2026-07-24 12:16:02,2026-07-24 12:16:02,1,120,open,
08936799,ITW,Itron,Cold water meter,itron,00,2026-07-24 12:16:09,2026-07-24 12:16:09,1,73,locked,
23252289,ITW,Itron,Water meter,itron,00,2026-07-24 12:16:17,2026-07-24 12:16:17,1,74,locked,
09155592,ITW,Itron,Cold water meter,itron,00,2026-07-24 12:16:31,2026-07-24 12:16:31,1,57,locked,
02713603,ITW,Itron,Cold water meter,itron,00,2026-07-24 12:17:05,2026-07-24 12:17:05,1,60,locked,
00340954,ITW,Itron,water,itron,00,2026-07-24 12:17:08,2026-07-24 12:17:08,1,85,open,
23252254,ITW,Itron,Water meter,itron,00,2026-07-24 12:17:08,2026-07-24 12:17:08,1,46,locked,
23252292,ITW,Itron,Water meter,itron,00,2026-07-24 12:17:09,2026-07-24 12:17:09,1,67,locked,
00227616,ITW,Itron,Cold water meter,itron,00,2026-07-24 12:17:21,2026-07-24 12:17:21,1,37,locked,
23252281,ITW,Itron,Water meter,itron,00,2026-07-24 12:17:33,2026-07-24 12:17:33,1,123,locked,
23252286,ITW,Itron,Water meter,itron,00,2026-07-24 12:17:54,2026-07-24 12:17:54,1,100,locked,
00093643,ITW,Itron,Cold water meter,itron,00,2026-07-24 12:18:07,2026-07-24 12:18:07,1,51,locked,
00124993,ITW,Itron,water,itron,00,2026-07-24 12:18:16,2026-07-24 12:18:16,1,34,open,
00065762,ITW,Itron,Cold water meter,itron,00,2026-07-24 12:18:31,2026-07-24 12:18:31,1,51,locked,
00257608,ITW,Itron,water,itron,00,2026-07-24 12:19:08,2026-07-24 12:19:08,1,39,open,
id,manufacturer,manufacturer_name,media,driver,version,first_seen,last_seen,count,rssi,interval_s,interval_samples,encryption,security_mode,status,aes_key
37287397,,,radio converter (meter side),qheatv2,,2026-07-24 12:35:10,2026-07-24 13:27:32,0,,,0,none,,open,
23252290,ITW,Itron,Water meter,itron,00,2026-07-24 12:35:17,2026-07-24 13:15:16,8,52,300.1,7,mode 5 (AES-CBC),5,locked,
00258334,ITW,Itron,water,itron,00,2026-07-24 12:36:02,2026-07-24 13:26:03,11,122,300.0,10,mode 5 (AES-CBC),5,locked,
61379612,,,warm water,qwaterv2,,2026-07-24 12:36:03,2026-07-24 13:26:46,0,,,0,none,,open,
08936799,ITW,Itron,Cold water meter,itron,00,2026-07-24 12:36:08,2026-07-24 13:21:08,10,83,300.2,9,mode 5 (AES-CBC),5,locked,
23252289,ITW,Itron,Water meter,itron,00,2026-07-24 12:36:17,2026-07-24 13:21:17,7,104,300.6,6,mode 5 (AES-CBC),5,locked,
61471660,QDS,Qundis,water,qwaterv2,1d,2026-07-24 12:36:21,2026-07-24 13:27:45,35,118,109.5,34,mode 5 (AES-CBC),5,locked,
09155592,ITW,Itron,Cold water meter,itron,00,2026-07-24 12:36:32,2026-07-24 13:21:32,10,51,300.2,9,mode 5 (AES-CBC),5,locked,
23252244,ITW,Itron,Water meter,itron,00,2026-07-24 12:36:49,2026-07-24 13:26:50,11,126,300.1,10,mode 5 (AES-CBC),5,locked,
02713603,ITW,Itron,Cold water meter,itron,00,2026-07-24 12:37:05,2026-07-24 13:17:06,9,60,300.1,8,mode 5 (AES-CBC),5,locked,
00340954,ITW,Itron,water,itron,00,2026-07-24 12:37:08,2026-07-24 13:17:08,9,83,300.0,8,mode 5 (AES-CBC),5,locked,
23252292,ITW,Itron,Water meter,itron,00,2026-07-24 12:37:09,2026-07-24 13:22:08,10,118,300.1,9,mode 5 (AES-CBC),5,locked,
68544330,,,heat,qheatv2,,2026-07-24 12:37:20,2026-07-24 13:22:13,0,,,0,none,,open,
23252281,ITW,Itron,Water meter,itron,00,2026-07-24 12:37:32,2026-07-24 13:27:32,11,101,300.1,10,mode 5 (AES-CBC),5,locked,
23252286,ITW,Itron,Water meter,itron,00,2026-07-24 12:37:53,2026-07-24 13:27:53,11,51,300.0,10,mode 5 (AES-CBC),5,locked,
00093643,ITW,Itron,Cold water meter,itron,00,2026-07-24 12:38:06,2026-07-24 13:23:05,10,97,300.1,9,mode 5 (AES-CBC),5,locked,
00124993,ITW,Itron,water,itron,00,2026-07-24 12:38:15,2026-07-24 13:23:15,10,78,300.0,9,mode 5 (AES-CBC),5,locked,
00065762,ITW,Itron,Cold water meter,itron,00,2026-07-24 12:38:30,2026-07-24 13:23:30,10,55,300.0,9,mode 5 (AES-CBC),5,locked,
00257608,ITW,Itron,water,itron,00,2026-07-24 12:39:08,2026-07-24 13:24:08,6,33,599.6,5,mode 5 (AES-CBC),5,locked,
10179914,ITW,Itron,water,itron,00,2026-07-24 12:39:11,2026-07-24 13:24:11,10,59,299.8,9,mode 5 (AES-CBC),5,locked,
23252389,ITW,Itron,Water meter,itron,00,2026-07-24 12:39:38,2026-07-24 13:24:38,9,122,300.1,8,mode 5 (AES-CBC),5,locked,
09237338,ITW,Itron,Cold water meter,itron,00,2026-07-24 12:39:51,2026-07-24 13:14:51,8,30,299.9,7,mode 5 (AES-CBC),5,locked,
23252380,ITW,Itron,Water meter,itron,00,2026-07-24 12:40:47,2026-07-24 13:25:47,4,60,599.9,3,mode 5 (AES-CBC),5,locked,
02548773,ITW,Itron,cold water,itron,00,2026-07-24 12:42:01,2026-07-24 13:22:00,9,49,299.8,8,mode 5 (AES-CBC),5,locked,
23252254,ITW,Itron,Water meter,itron,00,2026-07-24 12:42:08,2026-07-24 13:22:09,8,77,300.2,7,mode 5 (AES-CBC),5,locked,
09710983,ITW,Itron,Cold water meter,itron,00,2026-07-24 12:42:51,2026-07-24 13:07:51,4,34,300.0,3,mode 5 (AES-CBC),5,locked,
23252256,ITW,Itron,Water meter,itron,00,2026-07-24 12:43:31,2026-07-24 13:23:31,9,132,300.0,8,mode 5 (AES-CBC),5,locked,
23252283,ITW,Itron,Water meter,itron,00,2026-07-24 13:00:48,2026-07-24 13:15:48,4,67,299.9,3,mode 5 (AES-CBC),5,locked,
00227417,ITW,Itron,Cold water meter,itron,00,2026-07-24 13:05:16,2026-07-24 13:10:16,2,36,300.0,1,mode 5 (AES-CBC),5,locked,
00227616,ITW,Itron,Cold water meter,itron,00,2026-07-24 13:07:22,2026-07-24 13:17:22,3,29,300.0,2,mode 5 (AES-CBC),5,locked,
00287826,ITW,Itron,water,itron,00,2026-07-24 13:10:50,2026-07-24 13:15:51,2,37,300.3,1,mode 5 (AES-CBC),5,locked,
09710957,ITW,Itron,Cold water meter,itron,00,2026-07-24 13:11:07,2026-07-24 13:11:07,1,16,,0,mode 5 (AES-CBC),5,locked,
1 id manufacturer manufacturer_name media driver version first_seen last_seen count rssi interval_s interval_samples encryption security_mode status aes_key
2 23252256 37287397 ITW Itron Water meter radio converter (meter side) itron qheatv2 00 2026-07-24 12:13:32 2026-07-24 12:35:10 2026-07-24 12:18:31 2026-07-24 13:27:32 2 0 122 0 none locked open
3 61379612 23252290 ITW Itron warm water Water meter qwaterv2 itron 00 2026-07-24 12:13:38 2026-07-24 12:35:17 2026-07-24 12:19:30 2026-07-24 13:15:16 0 8 52 300.1 7 mode 5 (AES-CBC) 5 open locked
4 61471660 00258334 QDS ITW Qundis Itron water qwaterv2 itron 1d 00 2026-07-24 12:13:39 2026-07-24 12:36:02 2026-07-24 12:19:22 2026-07-24 13:26:03 5 11 124 122 300.0 10 mode 5 (AES-CBC) 5 open locked
5 10179914 61379612 ITW Itron water warm water itron qwaterv2 00 2026-07-24 12:14:11 2026-07-24 12:36:03 2026-07-24 12:19:11 2026-07-24 13:26:46 2 0 46 0 none open
6 23252389 08936799 ITW Itron Water meter Cold water meter itron 00 2026-07-24 12:14:38 2026-07-24 12:36:08 2026-07-24 12:19:38 2026-07-24 13:21:08 2 10 130 83 300.2 9 mode 5 (AES-CBC) 5 locked
7 37287397 23252289 ITW Itron radio converter (meter side) Water meter qheatv2 itron 00 2026-07-24 12:14:39 2026-07-24 12:36:17 2026-07-24 12:20:15 2026-07-24 13:21:17 0 7 104 300.6 6 mode 5 (AES-CBC) 5 open locked
8 09237338 61471660 ITW QDS Itron Qundis Cold water meter water itron qwaterv2 00 1d 2026-07-24 12:14:52 2026-07-24 12:36:21 2026-07-24 12:19:52 2026-07-24 13:27:45 2 35 61 118 109.5 34 mode 5 (AES-CBC) 5 locked
9 68544330 09155592 ITW Itron heat Cold water meter qheatv2 itron 00 2026-07-24 12:14:56 2026-07-24 12:36:32 2026-07-24 12:14:56 2026-07-24 13:21:32 0 10 51 300.2 9 mode 5 (AES-CBC) 5 open locked
10 23252290 23252244 ITW Itron Water meter itron 00 2026-07-24 12:15:16 2026-07-24 12:36:49 2026-07-24 12:15:16 2026-07-24 13:26:50 1 11 34 126 300.1 10 mode 5 (AES-CBC) 5 locked
11 00258334 02713603 ITW Itron water Cold water meter itron 00 2026-07-24 12:16:02 2026-07-24 12:37:05 2026-07-24 12:16:02 2026-07-24 13:17:06 1 9 120 60 300.1 8 mode 5 (AES-CBC) 5 open locked
12 08936799 00340954 ITW Itron Cold water meter water itron 00 2026-07-24 12:16:09 2026-07-24 12:37:08 2026-07-24 12:16:09 2026-07-24 13:17:08 1 9 73 83 300.0 8 mode 5 (AES-CBC) 5 locked
13 23252289 23252292 ITW Itron Water meter itron 00 2026-07-24 12:16:17 2026-07-24 12:37:09 2026-07-24 12:16:17 2026-07-24 13:22:08 1 10 74 118 300.1 9 mode 5 (AES-CBC) 5 locked
14 09155592 68544330 ITW Itron Cold water meter heat itron qheatv2 00 2026-07-24 12:16:31 2026-07-24 12:37:20 2026-07-24 12:16:31 2026-07-24 13:22:13 1 0 57 0 none locked open
15 02713603 23252281 ITW Itron Cold water meter Water meter itron 00 2026-07-24 12:17:05 2026-07-24 12:37:32 2026-07-24 12:17:05 2026-07-24 13:27:32 1 11 60 101 300.1 10 mode 5 (AES-CBC) 5 locked
16 00340954 23252286 ITW Itron water Water meter itron 00 2026-07-24 12:17:08 2026-07-24 12:37:53 2026-07-24 12:17:08 2026-07-24 13:27:53 1 11 85 51 300.0 10 mode 5 (AES-CBC) 5 open locked
17 23252254 00093643 ITW Itron Water meter Cold water meter itron 00 2026-07-24 12:17:08 2026-07-24 12:38:06 2026-07-24 12:17:08 2026-07-24 13:23:05 1 10 46 97 300.1 9 mode 5 (AES-CBC) 5 locked
18 23252292 00124993 ITW Itron Water meter water itron 00 2026-07-24 12:17:09 2026-07-24 12:38:15 2026-07-24 12:17:09 2026-07-24 13:23:15 1 10 67 78 300.0 9 mode 5 (AES-CBC) 5 locked
19 00227616 00065762 ITW Itron Cold water meter itron 00 2026-07-24 12:17:21 2026-07-24 12:38:30 2026-07-24 12:17:21 2026-07-24 13:23:30 1 10 37 55 300.0 9 mode 5 (AES-CBC) 5 locked
20 23252281 00257608 ITW Itron Water meter water itron 00 2026-07-24 12:17:33 2026-07-24 12:39:08 2026-07-24 12:17:33 2026-07-24 13:24:08 1 6 123 33 599.6 5 mode 5 (AES-CBC) 5 locked
21 23252286 10179914 ITW Itron Water meter water itron 00 2026-07-24 12:17:54 2026-07-24 12:39:11 2026-07-24 12:17:54 2026-07-24 13:24:11 1 10 100 59 299.8 9 mode 5 (AES-CBC) 5 locked
22 00093643 23252389 ITW Itron Cold water meter Water meter itron 00 2026-07-24 12:18:07 2026-07-24 12:39:38 2026-07-24 12:18:07 2026-07-24 13:24:38 1 9 51 122 300.1 8 mode 5 (AES-CBC) 5 locked
23 00124993 09237338 ITW Itron water Cold water meter itron 00 2026-07-24 12:18:16 2026-07-24 12:39:51 2026-07-24 12:18:16 2026-07-24 13:14:51 1 8 34 30 299.9 7 mode 5 (AES-CBC) 5 open locked
24 00065762 23252380 ITW Itron Cold water meter Water meter itron 00 2026-07-24 12:18:31 2026-07-24 12:40:47 2026-07-24 12:18:31 2026-07-24 13:25:47 1 4 51 60 599.9 3 mode 5 (AES-CBC) 5 locked
25 00257608 02548773 ITW Itron water cold water itron 00 2026-07-24 12:19:08 2026-07-24 12:42:01 2026-07-24 12:19:08 2026-07-24 13:22:00 1 9 39 49 299.8 8 mode 5 (AES-CBC) 5 open locked
26 23252254 ITW Itron Water meter itron 00 2026-07-24 12:42:08 2026-07-24 13:22:09 8 77 300.2 7 mode 5 (AES-CBC) 5 locked
27 09710983 ITW Itron Cold water meter itron 00 2026-07-24 12:42:51 2026-07-24 13:07:51 4 34 300.0 3 mode 5 (AES-CBC) 5 locked
28 23252256 ITW Itron Water meter itron 00 2026-07-24 12:43:31 2026-07-24 13:23:31 9 132 300.0 8 mode 5 (AES-CBC) 5 locked
29 23252283 ITW Itron Water meter itron 00 2026-07-24 13:00:48 2026-07-24 13:15:48 4 67 299.9 3 mode 5 (AES-CBC) 5 locked
30 00227417 ITW Itron Cold water meter itron 00 2026-07-24 13:05:16 2026-07-24 13:10:16 2 36 300.0 1 mode 5 (AES-CBC) 5 locked
31 00227616 ITW Itron Cold water meter itron 00 2026-07-24 13:07:22 2026-07-24 13:17:22 3 29 300.0 2 mode 5 (AES-CBC) 5 locked
32 00287826 ITW Itron water itron 00 2026-07-24 13:10:50 2026-07-24 13:15:51 2 37 300.3 1 mode 5 (AES-CBC) 5 locked
33 09710957 ITW Itron Cold water meter itron 00 2026-07-24 13:11:07 2026-07-24 13:11:07 1 16 0 mode 5 (AES-CBC) 5 locked