Wire Workstream K's summon-pipeline integration into G's ingestion handoff
Both G and K were built independently against the paper contract and correctly left this connection point for the integrator (documented in both their reports). Calls process_device_reading_for_summon from _process_reading, skipping array-valued readings (no defined single scalar baseline for those). Adds an end-to-end integration test that connects a real /ws/session, posts device telemetry through it, and confirms an anomalous reading actually reaches the live session as an anomaly_ack — proving the two independently-built pieces genuinely connect, not just that each compiles. 165/165 backend tests pass.
This commit is contained in:
@@ -4,8 +4,9 @@ docs/superpowers/specs/2026-07-23-esp32-sensor-node-design.md).
|
||||
|
||||
Scope boundary (per the spec): this module owns pairing, ingestion
|
||||
auth/validation, `last_seen_at` bookkeeping, and the in-process dashboard
|
||||
pub/sub broadcast. It does NOT feed readings into the séance/summon
|
||||
pipeline — that's Workstream K, which extends `_process_reading` below.
|
||||
pub/sub broadcast. Summon-pipeline anomaly detection (Workstream K) is
|
||||
wired in at `_process_reading` below, calling into
|
||||
`app.device_anomaly.process_device_reading_for_summon`.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
@@ -32,6 +33,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.db import async_session_maker as _default_session_maker
|
||||
from app.db import get_db
|
||||
from app.deps import SESSION_COOKIE_NAME, get_current_user
|
||||
from app.device_anomaly import process_device_reading_for_summon
|
||||
from app.models.auth_session import AuthSession, generate_session_token, hash_token
|
||||
from app.models.device import Device
|
||||
from app.models.user import User
|
||||
@@ -198,22 +200,28 @@ async def _broadcast_reading(device: Device, reading: Reading, at: datetime) ->
|
||||
async def _process_reading(device: Device, reading: Reading) -> None:
|
||||
"""Per-reading processing handoff point.
|
||||
|
||||
Workstream G (this file) intentionally leaves this a no-op: pairing,
|
||||
bearer auth, payload validation/size caps, per-device rate limiting, and
|
||||
the live dashboard broadcast (`_broadcast_reading`, above) are this
|
||||
workstream's full scope.
|
||||
Runs the reading through Workstream K's summon-pipeline integration
|
||||
(`process_device_reading_for_summon`): per-(user_id, device_id,
|
||||
sensor_type) rolling-baseline anomaly detection, pushing flagged
|
||||
anomalies into the owning user's active `SeanceState.anomalies` via the
|
||||
same path `_handle_anomaly` in `app/ws.py` already uses for the
|
||||
browser-based modes. A no-op if the user has no active séance session.
|
||||
|
||||
Workstream K's summon-pipeline integration extends/replaces this
|
||||
function to run per-(user_id, device_id, sensor_type) rolling-baseline
|
||||
anomaly detection and, when a reading is flagged, push
|
||||
`{"type": "anomaly", "source": <sensor_type>, "frequency": ...,
|
||||
"magnitude": ...}` into the owning user's active `SeanceState.anomalies`
|
||||
— the same path `_handle_anomaly` in `app/ws.py` already uses for the
|
||||
browser-based modes (`wire`/`evp`/`radio`/`emf`). It is async so that
|
||||
integration can await DB/session-registry work; called once per reading
|
||||
in `ingest_telemetry` below, in submission order.
|
||||
Array-valued readings (`reading.value: list[float]`) are skipped here —
|
||||
K's detector operates on a single scalar signal, and the contract
|
||||
doesn't define how a multi-element reading collapses to one baseline
|
||||
value. They still reach the live dashboard via `_broadcast_reading`
|
||||
below; only summon-pipeline anomaly detection is skipped for them.
|
||||
"""
|
||||
return None
|
||||
if isinstance(reading.value, list):
|
||||
return
|
||||
await process_device_reading_for_summon(
|
||||
user_id=device.user_id,
|
||||
device_id=device.id,
|
||||
sensor_type=reading.sensor_type,
|
||||
value=reading.value,
|
||||
unit=reading.unit,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/device/telemetry", status_code=status.HTTP_202_ACCEPTED)
|
||||
|
||||
Reference in New Issue
Block a user