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:
1 parent
2b6d43eb45
commit
66cb804102
11 files changed
+676
-15
No files matched your search
@@ -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}")
|
||||
|
||||
Reference in new issue
Block a user