Checklist: verify that sensor data reaches InfluxDB

New guided check "Sensor data in the database (InfluxDB)", also the last
step of the Quick check on the green path. It queries InfluxDB (read
token) for the newest point per measurement of the box and tells apart:
sensor data arrives / status arrives without sensor data / offline now /
never seen / check not possible. Polls every 30 s for up to 4 minutes.
With a stopped RTC in the log it concludes that sensor readings are lost
(stamped 1 Jan 2000): box A4:CB:8F:C9:47:3C sent only device_status and
microphone.

- service/influx.py: query, CSV parsing, classification (no new deps)
- guides: new step type "influx" (host answers via influx_result)
- Settings > InfluxDB access (or KOTA_INFLUX_TOKEN); token never logged
- service status mapping for tech.influx.* findings

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
KrzyszurandClaude Opus 5.5 committed 2026-10-01 00:06:48 +02:00
1 parent 2b6d43eb45
commit 66cb804102
11 files changed
+676 -15

No files matched your search

+66 -3
View File
@@ -4,7 +4,8 @@ GUI-independent. The runner is fed the same LogLines as the diagnostics
engine, reads the engine's report to route and to skip questions the log
already answers, asks the host to perform tool actions (board reset), and
records every answer and conclusion in the engine so they appear in the
report and the session log.
report and the session log. "influx" steps ask the host to query InfluxDB
(on_action("influx")); the host answers with influx_result().
"""
from __future__ import annotations
@@ -24,7 +25,9 @@ GUIDES_PATH = Path(__file__).with_name("guides.toml")
LED_ANSWERS = ["GREEN", "PURPLE", "RED", "BLUE", "WHITE", "YELLOW", "ORANGE", "CYAN", "OFF", "CYCLING"]
LED_ANSWER_LABELS = {**{k: v[0] for k, v in LED.items()},
"CYCLING": "Keeps changing / goes dark every few seconds"}
STEP_TYPES = {"led", "question", "info", "wait", "route"}
STEP_TYPES = {"led", "question", "info", "wait", "route", "influx"}
# Results of an InfluxDB check (service.influx.OUTCOMES).
INFLUX_OUTCOMES = ("ok", "sensors_missing", "silent", "never", "unavailable")
END_SEVERITY = {"ok": None, "info": Severity.INFO, "warning": Severity.WARNING,
"error": Severity.ERROR, "critical": Severity.CRITICAL}
@@ -58,9 +61,12 @@ class Step:
skip_unless_feature: str | None = None
skip_to: str | None = None
show_qr: bool = False # show the Matter pairing QR next to the step text
outcomes: dict[str, str] = field(default_factory=dict) # influx result -> target
poll_s: float = 30.0 # influx: query again after this many seconds
def targets(self) -> list[str]:
t = [o.next for o in self.options] + list(self.route.values()) + list(self.routes.values())
t += list(self.outcomes.values())
t += [x for x in (self.default, self.next, self.on_ok, self.on_fail, self.on_timeout, self.skip_to) if x]
return t
@@ -103,6 +109,7 @@ def load_guides(path: Path = GUIDES_PATH) -> dict[str, Guide]:
on_ok=s.get("on_ok"), on_fail=s.get("on_fail"), on_timeout=s.get("on_timeout"),
skip_if_finding=s.get("skip_if_finding"), skip_unless_feature=s.get("skip_unless_feature"),
skip_to=s.get("skip_to"), show_qr=bool(s.get("show_qr", False)),
outcomes=dict(s.get("outcomes", {})), poll_s=float(s.get("poll_s", 30)),
)
ends = {e["id"]: End(e["id"], e["severity"], e["title"], e["text"], tuple(e.get("actions", ())))
for e in g.get("end", [])}
@@ -123,6 +130,10 @@ def _validate(guides: dict[str, Guide]) -> None:
raise ValueError(f"{g.id}.{s.id}: unknown LED colours {set(s.route) - set(LED_ANSWERS)}")
if s.type == "wait" and not (s.until or s.until_milestone or s.pass_on_timeout):
raise ValueError(f"{g.id}.{s.id}: wait step needs until/until_milestone/pass_on_timeout")
if s.type == "influx" and set(INFLUX_OUTCOMES) - set(s.outcomes):
raise ValueError(f"{g.id}.{s.id}: influx step needs outcomes {set(INFLUX_OUTCOMES) - set(s.outcomes)}")
if set(s.outcomes) - set(INFLUX_OUTCOMES):
raise ValueError(f"{g.id}.{s.id}: unknown influx outcomes {set(s.outcomes) - set(INFLUX_OUTCOMES)}")
if s.tool_action not in (None, "reset"):
raise ValueError(f"{g.id}.{s.id}: unknown tool_action {s.tool_action!r}")
for t in s.targets():
@@ -191,6 +202,10 @@ class GuideRunner:
self._wait_started = 0.0
self._wait_boot = 0
self._wait_milestones: set[str] = set()
self._influx_pending = False
self._influx_next = 0.0
self._influx_last: str | None = None
self._influx_noted = ""
def start(self, guide_id: str) -> None:
self.cancel()
@@ -227,6 +242,28 @@ class GuideRunner:
elif s.type == "wait":
self._record(s, "technician skipped the wait")
self._finish_wait("timeout")
elif s.type == "influx":
self._record(s, "technician skipped the InfluxDB check")
self._finish_influx(self._influx_last or "unavailable")
def influx_result(self, outcome: str, summary: str, details: list[str] | tuple[str, ...] = ()) -> None:
"""Result of the host's InfluxDB query for the current influx step."""
s = self.step
if s is None or s.type != "influx" or not self._influx_pending:
return # stale answer: the technician moved on
self._influx_pending = False
self._influx_last = outcome
note = "InfluxDB: " + "; ".join([summary, *details])
if note != self._influx_noted: # polls repeat the same answer
self._influx_noted = note
self.engine.note_technician(note)
self._on_log(f"InfluxDB check: {summary}")
waited = self._clock() - self._wait_started
if outcome in ("ok", "unavailable") or waited >= s.timeout_s:
self._finish_influx(outcome)
else:
self._influx_next = self._clock() + s.poll_s
self.note = f"{summary}. Checking again in {s.poll_s:.0f} s..."
def back(self) -> None:
# Never step back into a wait: re-entering it could restart the box.
@@ -256,6 +293,11 @@ class GuideRunner:
s = self.step
if s is not None and s.type == "wait" and self._clock() - self._wait_started >= s.timeout_s:
self._finish_wait("ok" if s.pass_on_timeout else "timeout")
elif s is not None and s.type == "influx" and not self._influx_pending:
if self._clock() - self._wait_started >= s.timeout_s:
self._finish_influx(self._influx_last or "unavailable")
elif self._clock() >= self._influx_next:
self._request_influx()
# --------------------------------------------------------------- view
def view(self) -> View:
@@ -268,7 +310,7 @@ class GuideRunner:
v.options = [(k, LED_ANSWER_LABELS[k]) for k in LED_ANSWERS if k in s.route]
elif s.type == "question":
v.options = [(str(i), o.label) for i, o in enumerate(s.options)]
elif s.type == "wait":
elif s.type in ("wait", "influx"):
v.waiting = True
v.remaining_s = max(0.0, s.timeout_s - (self._clock() - self._wait_started))
return v
@@ -316,6 +358,27 @@ class GuideRunner:
if s.until_milestone and not s.tool_action and s.until_milestone in self.engine.milestones:
self._record(s, f"already reached: {s.until_milestone}")
self._finish_wait("ok")
if s.type == "influx":
self._wait_started = self._clock()
self._influx_last = None
self._request_influx()
def _request_influx(self) -> None:
self._influx_pending = True
if not self._on_action("influx"):
self._influx_pending = False
self._influx_last = "unavailable"
self._record(self.step, "InfluxDB check not possible from this computer")
self._finish_influx("unavailable")
def _finish_influx(self, outcome: str) -> None:
s = self.step
self._influx_pending = False
target = s.outcomes.get(outcome)
if target is None:
return
self._push()
self._go(target)
def _auto(self, s: Step, why: str, target: str | None) -> None:
self._on_log(f"technician guide: {s.id}: {why}")