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>
This commit is contained in:
79
backend/app/services/signal_history_service.py
Normal file
79
backend/app/services/signal_history_service.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user