localeye.cam

← Back

module.py

doors version 2.5 · 4 KB

1"""Doors custom module: ICT Protege WX door events linked to cameras.
2
3web(api) the Doors page (/x/doors/) and its API (/api/x/doors/...)
4recorder(api) the poller that reads the controller's live event feed (runs in the recorder service)
5"""
6from datetime import datetime, timedelta
7
8from flask import abort, request
9
10from .doors_events import KIND_LABELS, DoorStore, ProtegePoller
11
12
13def recorder(api):
14 return ProtegePoller(api)
15
16
17def _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
27def _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
33def 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}