Files
the-other-dude/backend/app/services/signal_history_service.py
Jason Staack 124a72582b feat(15-01): add signal history and site alert services, routers, and main.py wiring
- Create signal_history_service with TimescaleDB time_bucket queries for 24h/7d/30d ranges
- Create site_alert_service with full CRUD for rules, events list/resolve, and active count
- Create signal_history router with GET endpoint for time-bucketed signal data
- Create site_alerts router with CRUD endpoints for rules and event management
- Wire both routers into main.py with /api prefix

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 07:18:02 -05:00

80 lines
2.5 KiB
Python

"""Signal history service -- time-bucketed signal strength queries.
Uses raw SQL with TimescaleDB time_bucket() for efficient time-series aggregation.
All queries run via the app_user engine (RLS enforced).
"""
import uuid
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from app.schemas.site_alert import SignalHistoryPoint, SignalHistoryResponse
# Mapping of range parameter to (time_bucket interval, lookback interval)
RANGE_CONFIG = {
"24h": ("5 minutes", "24 hours"),
"7d": ("1 hour", "7 days"),
"30d": ("4 hours", "30 days"),
}
async def get_signal_history(
db: AsyncSession,
tenant_id: uuid.UUID,
device_id: uuid.UUID,
mac_address: str,
range: str = "7d",
) -> SignalHistoryResponse:
"""Query time-bucketed signal history for a specific client MAC on a device.
Args:
db: Database session (app_user with RLS).
tenant_id: Tenant UUID for RLS context.
device_id: Device UUID (the AP the client connects to).
mac_address: Client MAC address to query history for.
range: Time range -- "24h", "7d", or "30d".
Returns:
SignalHistoryResponse with time-bucketed signal avg/min/max.
"""
bucket_interval, lookback = RANGE_CONFIG.get(range, RANGE_CONFIG["7d"])
result = await db.execute(
text(f"""
SELECT
time_bucket(:bucket_interval, wr.time) AS bucket,
avg(wr.signal_strength)::int AS signal_avg,
min(wr.signal_strength) AS signal_min,
max(wr.signal_strength) AS signal_max
FROM wireless_registrations wr
WHERE wr.mac_address = :mac_address
AND wr.device_id = :device_id
AND wr.tenant_id = :tenant_id
AND wr.time > now() - :lookback::interval
AND wr.signal_strength IS NOT NULL
GROUP BY bucket
ORDER BY bucket
"""),
{
"bucket_interval": bucket_interval,
"lookback": lookback,
"mac_address": mac_address,
"device_id": str(device_id),
"tenant_id": str(tenant_id),
},
)
rows = result.fetchall()
items = [
SignalHistoryPoint(
timestamp=row.bucket,
signal_avg=row.signal_avg,
signal_min=row.signal_min,
signal_max=row.signal_max,
)
for row in rows
]
return SignalHistoryResponse(items=items, mac_address=mac_address, range=range)