"""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: #kota-report= 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"))