module.py
doors version 2.5 · 4 KB
| 1 | """Doors custom module: ICT Protege WX door events linked to cameras. |
| 2 | |
| 3 | web(api) the Doors page (/x/doors/) and its API (/api/x/doors/...) |
| 4 | recorder(api) the poller that reads the controller's live event feed (runs in the recorder service) |
| 5 | """ |
| 6 | from datetime import datetime, timedelta |
| 7 | |
| 8 | from flask import abort, request |
| 9 | |
| 10 | from .doors_events import KIND_LABELS, DoorStore, ProtegePoller |
| 11 | |
| 12 | |
| 13 | def recorder(api): |
| 14 | return ProtegePoller(api) |
| 15 | |
| 16 | |
| 17 | def _day(value): |
| 18 | if not value: |
| 19 | return None, None |
| 20 | try: |
| 21 | day = datetime.strptime(value, "%Y-%m-%d") |
| 22 | except ValueError: |
| 23 | abort(400) |
| 24 | return day.timestamp(), (day + timedelta(days=1)).timestamp() |
| 25 | |
| 26 | |
| 27 | def _filters(a): |
| 28 | start_ts, end_ts = _day(a.get("date")) |
| 29 | return {"q": a.get("q") or None, "kind": a.get("kind") or None, "door": a.get("door") or None, |
| 30 | "start_ts": start_ts, "end_ts": end_ts} |
| 31 | |
| 32 | |
| 33 | def web(api): |
| 34 | store = DoorStore(api.db) |
| 35 | |
| 36 | @api.route("/") |
| 37 | def page(): |
| 38 | return api.render("doors.html") |
| 39 | |
| 40 | @api.route("/api/events") |
| 41 | def events(): |
| 42 | a = request.args |
| 43 | try: |
| 44 | limit = min(max(int(a.get("limit", 100)), 1), 500) |
| 45 | except ValueError: |
| 46 | limit = 100 |
| 47 | rows = store.events(**_filters(a), before_id=a.get("before_id") if (a.get("before_id") or "").isdigit() else None, |
| 48 | limit=limit) |
| 49 | names = api.camera_names() |
| 50 | for r in rows: |
| 51 | r["time"] = datetime.fromtimestamp(r["ts"]).isoformat(timespec="seconds") |
| 52 | r["kind_label"] = KIND_LABELS.get(r["kind"], r["kind"]) |
| 53 | r["camera_names"] = [names.get(c, c) for c in r["cameras"]] |
| 54 | return {"ok": True, "events": rows, "doors": store.door_names(), |
| 55 | "setup": {"protege": api.recorder_status(), "mappings": api.settings().get("mappings") or [], |
| 56 | "cameras": api.cameras()}} |
| 57 | |
| 58 | @api.route("/api/mappings", methods=["POST"]) |
| 59 | def mappings(): |
| 60 | rows = (request.get_json(silent=True) or {}).get("mappings") or [] |
| 61 | cams = {c["stream"] for c in api.cameras()} |
| 62 | clean = [] |
| 63 | for m in rows[:200] if isinstance(rows, list) else []: |
| 64 | if not isinstance(m, dict): |
| 65 | continue |
| 66 | match, cam = " ".join(str(m.get("match") or "").split())[:80], str(m.get("camera") or "") |
| 67 | if match and cam in cams: |
| 68 | clean.append({"match": match, "camera": cam}) |
| 69 | api.save_settings({"mappings": clean}) |
| 70 | api.audit("mappings_saved", "", count=len(clean)) |
| 71 | return {"ok": True, "mappings": clean} |
| 72 | |
| 73 | @api.route("/api/test", methods=["POST"]) |
| 74 | def test(): |
| 75 | r = api.recorder("test") |
| 76 | if not r: |
| 77 | return {"ok": False, "error": "Recorder service unreachable"}, 502 |
| 78 | api.audit("protege_test", "", ok=r.get("ok")) |
| 79 | return r |
| 80 | |
| 81 | @api.route("/api/events/<int:event_id>", methods=["DELETE"]) |
| 82 | def delete_one(event_id): |
| 83 | ok = store.delete(event_id) |
| 84 | if ok: |
| 85 | api.audit("event_deleted", str(event_id)) |
| 86 | return {"ok": ok} |
| 87 | |
| 88 | @api.route("/api/delete_matching", methods=["POST"]) |
| 89 | def delete_matching(): |
| 90 | """Delete every door event matching the current filters. Send {"confirm": true} to actually delete; without it, |
| 91 | returns the count so the page can confirm first.""" |
| 92 | body = request.get_json(silent=True) or {} |
| 93 | f = _filters(body) |
| 94 | if not body.get("confirm"): |
| 95 | return {"ok": True, "count": store.count(**f)} |
| 96 | n = store.delete_matching(**f) |
| 97 | api.audit("events_deleted", "", count=n, filters={k: v for k, v in f.items() if v is not None}) |
| 98 | return {"ok": True, "deleted": n} |