Files
StarPilot/openpilot/starpilot/galaxy/sentry_events.py
T
Danny d08cd1b08b Galaxy: Add remote keys and app Sentry notifications
Serve the Galaxy app key and its QR code from /api/galaxy/session to direct local connections so the Android app can import the tunnel key on its own. Let the app and the iOS app register for Sentry push alerts.
2026-10-27 02:46:50 -04:00

70 lines
3.0 KiB
Python

"""Bounded projection of local Sentry motion metadata."""
import json
import re
from collections.abc import Callable
from typing import Protocol, cast
from openpilot.starpilot.sentry_mode.storage import EventStore, StorageUnavailable
MAX_RESPONSE_BYTES = 128 * 1024
ID = re.compile(r"[0-9a-f]{32}\Z")
class SentryEventsUnavailable(Exception):
pass
class EventSnapshotSource(Protocol):
def snapshot(self) -> dict: ...
class EventDeletionSource(Protocol):
def delete(self, event_id: str | None = None, *, permitted: Callable[[], bool]) -> dict: ...
class SentryEvents:
def __init__(self, store: EventSnapshotSource | None = None):
self.store = store if store is not None else EventStore()
def snapshot(self) -> dict:
try:
source = self.store.snapshot()
if (type(source) is not dict or type(source.get("version")) is not int or source["version"] != 1 or
type(source.get("incomplete")) is not bool or type(source.get("capacity")) is not int or source["capacity"] != 512 or
type(source.get("events")) is not list or len(source["events"]) > 512):
raise ValueError("Invalid motion inventory")
events = []
for value in source["events"]:
if (type(value) is not dict or type(value.get("eventId")) is not str or
ID.fullmatch(value["eventId"]) is None or type(value.get("kind")) is not str or
value["kind"] not in ("warning", "alarm", "selfie") or
type(value.get("wallTimeNs")) is not int or not 0 < value["wallTimeNs"] < 2**63):
raise ValueError("Invalid motion event")
events.append({"eventId": value["eventId"], "kind": value["kind"],
"systemTimeMs": value["wallTimeNs"] // 1_000_000,
**({"simulated": True} if value.get("simulated") else {}),
"images": cast(Callable[[str], list], self.store.images)(value["eventId"]) if hasattr(self.store, "images") else []})
result = {"schemaVersion": 1, "source": "local", "scanIncomplete": source["incomplete"],
"capacity": source["capacity"], "events": events,
"storageBytes": source.get("storageBytes", 0), "storageIncomplete": source.get("storageIncomplete", False)}
if len(json.dumps(result, separators=(",", ":")).encode()) > MAX_RESPONSE_BYTES:
raise ValueError("Motion response exceeds limit")
return result
except (StorageUnavailable, OSError, ValueError, TypeError, OverflowError, RecursionError) as error:
raise SentryEventsUnavailable from error
def image(self, event_id, camera):
try:
return self.store.image(event_id, camera)
except (OSError, StorageUnavailable, AttributeError) as error:
raise SentryEventsUnavailable from error
def delete(self, event_id=None, *, permitted=lambda: True):
try:
result = cast(EventDeletionSource, self.store).delete(event_id, permitted=permitted)
return {**self.snapshot(), **result}
except (StorageUnavailable, OSError, AttributeError) as error:
raise SentryEventsUnavailable from error