Skip to content

Commit c29eaac

Browse files
committed
docs: document the Home Assistant integration
User-facing + contributor docs for the integration shipped in Phases 1–3 (+1b). No code/behavior change. - New /docs page "Home Assistant" (frontend/src/pages/docs/HomeAssistant.jsx) wired into DocsPage (import, scrollspy section, Integrations sidebar group, render order): what you get, the 3-step setup (generate osi_ key → install HACS component → add + paste URL/key), and notes (LAN-direct video, revoke, MCP-vs-HA key distinction). - AGENTS.md: integration.py in the structure tree; an "Integration API key" auth subsection (osi_, kind filter, one-shot session); kind column on the McpApiKey model row; integration.py in the router-prefix table; full /api/integration/* endpoint block. - README.md: feature bullet + integration-key note in the auth paragraph. - plans/home-assistant-integration.md: status table (Phases 1–3 + 1b shipped, Phase 4 built in the separate repo, 2b deferred). - rebuilt backend/static bundle. Lint clean, vitest 99 passed, build clean.
1 parent 1e8f08b commit c29eaac

8 files changed

Lines changed: 161 additions & 10 deletions

File tree

AGENTS.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ backend/
8888
│ │ ├── mcp_keys.py # MCP API key management + tool catalog + audit
8989
│ │ │ # notifications (mcp_key_created/revoked)
9090
│ │ ├── mcp_activity.py # MCP activity logs, stats, SSE stream
91+
│ │ ├── integration.py # Home Assistant REST API (osi_ keys): key mgmt +
92+
│ │ │ # camera discovery, snapshot, recording, status, motion SSE
9193
│ │ ├── motion.py # Motion event queries, stats, SSE stream
9294
│ │ ├── notifications.py # Notification inbox, unread count, SSE, broadcaster,
9395
│ │ │ # email kind map, email cooldown gate (motion v1.1),
@@ -264,9 +266,24 @@ CloudNode endpoints validate `X-Node-API-Key`:
264266

265267
MCP endpoint (`POST /mcp`) validates `Authorization: Bearer osc_<hex>`:
266268
1. SHA-256 hash the raw key
267-
2. Match against `McpApiKey.key_hash` with `revoked=False`
269+
2. Match against `McpApiKey.key_hash` with `revoked=False` AND `kind="mcp"`
268270
3. `ScopeMiddleware` (see below) filters tool discovery + invocation per-key
269271

272+
### Integration API key
273+
274+
`/api/integration/*` endpoints (Home Assistant) validate `Authorization: Bearer osi_<hex>`
275+
via `core/integration_auth.py::require_integration_org`:
276+
1. SHA-256 hash the raw key
277+
2. Match against `McpApiKey.key_hash` with `revoked=False` AND `kind="integration"`
278+
3. Derive `org_id` from the matched row
279+
280+
Integration keys share the `mcp_api_keys` table with MCP keys, split by `kind`
281+
(`mcp` vs `integration`). The `kind` filter is applied to **every** `McpApiKey`
282+
query — both auth paths and both management list/revoke endpoints — so the two
283+
key kinds can never cross surfaces. `require_integration_org` uses a **one-shot
284+
DB session** (not `Depends(get_db)`) so the long-lived motion SSE doesn't pin a
285+
connection for its lifetime. No plan gate — available on all tiers.
286+
270287
## Data Models
271288

272289
All 20 models in `backend/app/models/models.py`. Every model has `org_id` for tenant isolation EXCEPT `ProcessedWebhook` (global webhook dedup, keyed on Svix msg_id) and `EmailSuppression` (operator-global bounce / complaint list, intentionally cross-tenant so a hard-bounced address stops being mailed for every org).
@@ -279,7 +296,7 @@ All 20 models in `backend/app/models/models.py`. Every model has `org_id` for te
279296
| `Setting` | `key`, `value` | Per-org key/value store. Used for plan slug, recording config, email toggles, motion email cooldown anchors (`motion_email_cooldown_start:{camera_id}`), CloudNode disk debounce (`cloudnode_disk_low_emit_at:{node_id}`), payment past-due flags. |
280297
| `AuditLog` | `event`, `user_id`, `ip_address`, `details`, `(org_id, timestamp)` index | Security audit trail |
281298
| `StreamAccessLog` | `user_id`, `camera_id`, `ip_address`, `user_agent`, `accessed_at`, `(org_id, accessed_at)` index | Stream playback audit |
282-
| `McpApiKey` | `name`, `key_hash`, `scope_mode`, `scope_tools` (JSON text), `last_used_at`, `revoked` | MCP API keys **scope_mode**: `all` / `readonly` / `custom` |
299+
| `McpApiKey` | `name`, `key_hash`, `kind` (`mcp`/`integration`), `scope_mode`, `scope_tools` (JSON text), `last_used_at`, `revoked` | Both MCP keys (`osc_`, AI agents) AND Home Assistant integration keys (`osi_`) — one table, split by `kind`; **scope_mode** (MCP only): `all` / `readonly` / `custom` |
283300
| `McpActivityLog` | `tool_name`, `key_name`, `status`, `duration_ms`, `args_summary`, `error`, `timestamp`, `(org_id, timestamp)` index | Per-call MCP audit log |
284301
| `Incident` | `title`, `summary`, `report` (markdown), `severity`, `status`, `camera_id`, `created_by`, `resolved_at`, `resolved_by` | AI-generated incident (`open` / `acknowledged` / `resolved` / `dismissed`) |
285302
| `IncidentEvidence` | `incident_id` (FK cascade), `kind` (`snapshot` / `clip` / `observation`), `text`, `camera_id`, `data` (LargeBinary), `data_mime` | Snapshot JPEG, clip (MPEG-TS bytes), or text observation — evidence travels inline with the incident |
@@ -311,6 +328,7 @@ Validation constants (also in `models.py`):
311328
| `incidents.py` | `/api/incidents` | incidents |
312329
| `mcp_keys.py` | `/api/mcp` | mcp |
313330
| `mcp_activity.py` | `/api/mcp/activity` | mcp-activity |
331+
| `integration.py` | `/api/integration` | integration |
314332
| `motion.py` | `/api/motion` | motion |
315333
| `notifications.py` | `/api/notifications` | notifications |
316334
| `sentinel.py` | `/api/sentinel` | sentinel |
@@ -388,6 +406,16 @@ Validation constants (also in `models.py`):
388406
- `GET /logs` — filterable MCP tool call log (admin)
389407
- `GET /logs/stats` — summary counts for logs (admin)
390408

409+
**integration.py** (prefix `/api/integration`) — Home Assistant; key mgmt is admin (Clerk JWT), data plane is `osi_` integration-key auth (all tiers):
410+
- `POST /keys` — mint an `osi_` integration key; returns plaintext once (admin)
411+
- `GET /keys` — list integration keys (admin)
412+
- `DELETE /keys/{key_id}` — revoke (admin)
413+
- `GET /cameras` — all cameras across all nodes with LAN-direct `local_url`, snapshot URL, recording state (integration key)
414+
- `GET /cameras/{id}/snapshot` — live JPEG via the node round-trip (integration key)
415+
- `POST /cameras/{id}/recording` — toggle `continuous_24_7`; body `{recording: bool}` (integration key)
416+
- `GET /status` — org rollup: camera/node online counts, per-node disk + version, plan (integration key)
417+
- `GET /motion/stream` — SSE motion feed for HA `binary_sensor`s; separate subscriber pool from the dashboard's `/api/motion/events/stream` (integration key)
418+
391419
**motion.py** (prefix `/api/motion`):
392420
- `GET /events` — list motion events; filters: `camera_id`, `hours`, `limit`, `offset` (view)
393421
- `GET /events/stats` — per-camera aggregates (view)

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Sentinel Command Center is the cloud hub for the Sentinel ecosystem (Sentinel by
3232
- **Email notifications** via Resend — opt-in per-org per-kind, with per-camera cooldown + digest mode for high-volume motion events (14 notification kinds gated by 7 setting toggles; plus a hidden `welcome` kind that has no UI toggle)
3333
- Audit logging for stream access, admin actions, and MCP tool calls
3434
- MCP server exposing 23 tools (16 read, 7 write) so AI clients can view cameras, file incident reports with snapshots and short video clips, and read back past investigations — with per-key scoping (all / readonly / custom allow-list)
35+
- **Home Assistant integration** — connect Home Assistant with a single key; every camera across every node appears with live video (LAN-direct, so it never counts against the viewer-hour cap), snapshots, recording control, and real-time motion triggers. Available on **every plan**. REST surface under `/api/integration/*`, consumed by a HACS custom component (separate repo)
3536
- **Sentinel AI agent** — webhook-driven serverless agent ([separate repo](https://github.com/SourceBox-LLC/SourceBox-Sentinel)) that auto-investigates motion events and incident_opened notifications using a vision-capable LLM ↔ MCP tool loop. Pro: 100 runs/month. Pro Plus: 500 runs/month. Per-org scoping via signed override header — one deployed agent serves every org with no cross-tenant state.
3637

3738
---
@@ -110,7 +111,7 @@ The scripts detect which clients you already have and merge an `opensentry` entr
110111

111112
**Video pipeline:** CloudNode transcodes USB camera video into HLS segments and pushes each `.ts` file directly to the backend via `POST /api/cameras/{id}/push-segment`. The backend caches segments in memory (60 per camera by default, ~60s buffer) and serves them through the same-origin proxy at `GET /api/cameras/{id}/segment/{file}`. The rewritten playlist contains relative segment URLs, so the browser's Clerk JWT auth header is automatically attached. No S3, no presigned URLs, no third-party storage in the live path. A global byte ceiling (`SEGMENT_CACHE_MAX_TOTAL_BYTES`, default 2 GiB) bounds total cache size across all cameras and evicts oldest-first when exceeded.
112113

113-
**Authentication:** Clerk handles user sign-up, login, and organization management. The backend validates JWT tokens (V1 and V2 permission formats) and extracts organization-scoped permissions. CloudNodes authenticate with API keys (SHA-256 hashed in the database) passed via `X-Node-API-Key`. MCP clients authenticate with `Authorization: Bearer osc_...` keys (also hashed).
114+
**Authentication:** Clerk handles user sign-up, login, and organization management. The backend validates JWT tokens (V1 and V2 permission formats) and extracts organization-scoped permissions. CloudNodes authenticate with API keys (SHA-256 hashed in the database) passed via `X-Node-API-Key`. MCP clients authenticate with `Authorization: Bearer osc_...` keys (also hashed). Home Assistant authenticates with `osi_...` integration keys (same `mcp_api_keys` table, distinguished by a `kind` column; available on all plans).
114115

115116
**Storage:** Live segments live in the backend's in-memory cache; they expire automatically once `SEGMENT_CACHE_MAX_PER_CAMERA` is exceeded. Recordings and snapshots live on the CloudNode itself. **SQLite** is the production database (single-machine deploy on a Fly volume at `/data`); `DATABASE_URL` defaults to `sqlite:///./opensentry.db` for local dev. Incident snapshots and clips are stored inline on `IncidentEvidence.data` (LargeBinary) — evidence travels with the incident.
116117

backend/static/assets/DocsPage-D4HUmfV1.js renamed to backend/static/assets/DocsPage-BCzUh3fS.js

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)