102 lines
4.4 KiB
Python
102 lines
4.4 KiB
Python
"""Hand-off of a diagnosis to the KOTA service system (Google Apps Script).
|
|
|
|
The tool opens the box's service page in the technician's browser:
|
|
<base><MAC>#kota-report=<base64url(JSON)>
|
|
The service page (v1.1.0+) reads the fragment and pre-fills status and note;
|
|
the technician checks it, adds the location and saves - so the report is
|
|
authored by their own Google login and the tool holds no credentials.
|
|
The fragment is never sent to any server (redirect host or Apps Script).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
import re
|
|
|
|
from .. import __version__
|
|
from ..diag.engine import Report, Severity
|
|
|
|
DEFAULT_BASE = "https://service.kota-app.com/"
|
|
NOTE_LIMIT = 4000 # the service form accepts 5000; leave room for the technician
|
|
|
|
# Status names exactly as in the service system (Code.gs STATUSES).
|
|
OK = "Działa poprawnie"
|
|
CHECK = "Do sprawdzenia"
|
|
IN_SERVICE = "W trakcie serwisu"
|
|
HARDWARE = "Błąd hardware"
|
|
SOFTWARE = "Błąd software"
|
|
POWER = "Problem z zasilaniem"
|
|
CONNECTIVITY = "Problem z łącznością"
|
|
|
|
# Root-finding rule id prefix -> service status (first match wins).
|
|
_STATUS_BY_RULE = [
|
|
("reboot.brownout", POWER), ("reboot.power", POWER), ("tech.power.", POWER),
|
|
("sys.crash", SOFTWARE), ("reboot.watchdog", SOFTWARE), ("sys.out_of_resources", SOFTWARE),
|
|
("influx.bad_request", SOFTWARE), ("ota.selfcheck_failed", SOFTWARE), ("tech.quick.hung", SOFTWARE),
|
|
("rtc.init_failed", HARDWARE), ("rtc.lost_after_sync", HARDWARE), ("i2c.", HARDWARE), ("sensor.", HARDWARE),
|
|
("sd.", HARDWARE), ("sen66.", HARDWARE), ("bme688.", HARDWARE), ("as7341.", HARDWARE),
|
|
("mic.read_fail", HARDWARE), ("mic.silent", HARDWARE), ("tech.rtc_i2c.", HARDWARE), ("tech.sensors.", HARDWARE),
|
|
("tech.mic.mic_faulty", HARDWARE), ("tech.mic.mic_switch_faulty", HARDWARE), ("tech.white.sd_", HARDWARE),
|
|
("matter.", CONNECTIVITY), ("net.", CONNECTIVITY), ("wifi.", CONNECTIVITY), ("influx.", CONNECTIVITY),
|
|
("ota.", CONNECTIVITY), ("tech.network.", CONNECTIVITY),
|
|
("rtc.clock_not_running", CHECK), ("reboot.", CHECK),
|
|
]
|
|
_MAC = re.compile(r"^[0-9A-F]{12}$")
|
|
|
|
|
|
def normalize_mac(mac: str) -> str:
|
|
return re.sub(r"[^0-9A-Fa-f]", "", mac or "").upper()
|
|
|
|
|
|
def service_status(report: Report) -> str:
|
|
"""Suggested status; the technician can change it in the form."""
|
|
problems = [rc for rc in report.roots if rc.severity >= Severity.WARNING]
|
|
if not problems:
|
|
return OK if any(m == "data_uploaded" and t is not None for m, _, t in report.milestones) else CHECK
|
|
rid = problems[0].finding.rule_id
|
|
for prefix, status in _STATUS_BY_RULE:
|
|
if rid.startswith(prefix):
|
|
return status
|
|
return CHECK
|
|
|
|
|
|
def service_note(report: Report, limit: int = NOTE_LIMIT) -> str:
|
|
lines = [f"[KOTA Technician Tool {__version__}] {report.verdict}",
|
|
f"Firmware: {report.firmware.describe()}",
|
|
f"Expected LED: {report.led.capitalize()}"]
|
|
reached = [label for _, label, t in report.milestones if t is not None]
|
|
missing = [label for _, label, t in report.milestones if t is None]
|
|
if reached:
|
|
lines.append("OK: " + ", ".join(reached))
|
|
if missing:
|
|
lines.append("Not reached: " + ", ".join(missing))
|
|
for rc in report.roots:
|
|
f = rc.finding
|
|
lines.append(f"- [{f.severity.label}] {f.title}")
|
|
for c in rc.consequences:
|
|
lines.append(f" -> {c.title}")
|
|
if report.technician:
|
|
lines.append("Technician checks:")
|
|
lines += [f" * {t}" for t in report.technician[-12:]]
|
|
text = "\n".join(lines)
|
|
return text if len(text) <= limit else text[:limit - 20].rstrip() + "\n...(shortened)"
|
|
|
|
|
|
def report_url(mac: str, report: Report, base: str = DEFAULT_BASE) -> str:
|
|
mac = normalize_mac(mac)
|
|
if not _MAC.match(mac):
|
|
raise ValueError(f"not a box MAC address: {mac!r}")
|
|
payload = {"v": 1, "mac": mac, "status": service_status(report), "note": service_note(report),
|
|
"tool": f"v{__version__}"}
|
|
raw = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
|
frag = base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=")
|
|
base = base if base.endswith("/") else base + "/"
|
|
return f"{base}{mac}#kota-report={frag}"
|
|
|
|
|
|
def decode_fragment(url: str) -> dict:
|
|
"""Inverse of report_url (used by tests and for troubleshooting)."""
|
|
frag = url.split("#kota-report=", 1)[1]
|
|
return json.loads(base64.urlsafe_b64decode(frag + "=" * (-len(frag) % 4)).decode("utf-8"))
|