feat: heartbeat detection and sensor metrics

- Heartbeat service: check_sensor_status (online/stale/offline)
- GET /sensors/heartbeat: status summary for all sensors
- POST /sensors/heartbeat: refresh heartbeat status
- GET /sensors/<hostname>/metrics: activity counts, recent events
- CLI command: flask check-heartbeats
- Added 7 new tests (34 total)
This commit is contained in:
user
2026-02-05 21:23:55 +01:00
parent 4b72b3293e
commit b36b1579c7
5 changed files with 286 additions and 4 deletions

View File

@@ -1,10 +1,12 @@
"""Sensor endpoints."""
import json
import socket
from datetime import datetime, timedelta, UTC
from flask import request, current_app
from . import bp
from ..models import Sensor
from ..models import Sensor, Event, Sighting, Alert
from ..extensions import db
from ..services.heartbeat import get_heartbeat_summary, update_all_heartbeats
@bp.route('/sensors')
@@ -213,3 +215,73 @@ def trigger_calibrate(hostname):
return {'error': f'Socket error: {e}'}, 500
return {'status': 'calibration_started', 'seconds': seconds}
@bp.route('/sensors/heartbeat')
def get_heartbeat_status():
"""Get heartbeat status for all sensors."""
return get_heartbeat_summary()
@bp.route('/sensors/heartbeat', methods=['POST'])
def refresh_heartbeats():
"""Update heartbeat status for all sensors."""
counts = update_all_heartbeats()
return {
'status': 'updated',
'online': counts['online'],
'stale': counts['stale'],
'offline': counts['offline']
}
@bp.route('/sensors/<hostname>/metrics')
def get_sensor_metrics(hostname):
"""Get sensor activity metrics and recent events."""
sensor = db.session.scalar(db.select(Sensor).where(Sensor.hostname == hostname))
if not sensor:
return {'error': 'Sensor not found'}, 404
# Time range (default: last 24 hours)
hours = request.args.get('hours', 24, type=int)
if hours < 1 or hours > 168: # max 1 week
hours = 24
since = datetime.now(UTC) - timedelta(hours=hours)
# Count activity
sightings_count = db.session.scalar(
db.select(db.func.count(Sighting.id))
.where(Sighting.sensor_id == sensor.id)
.where(Sighting.timestamp >= since)
) or 0
alerts_count = db.session.scalar(
db.select(db.func.count(Alert.id))
.where(Alert.sensor_id == sensor.id)
.where(Alert.timestamp >= since)
) or 0
events_count = db.session.scalar(
db.select(db.func.count(Event.id))
.where(Event.sensor_id == sensor.id)
.where(Event.timestamp >= since)
) or 0
# Recent events (last 20)
recent_events = db.session.scalars(
db.select(Event)
.where(Event.sensor_id == sensor.id)
.order_by(Event.timestamp.desc())
.limit(20)
).all()
return {
'hostname': sensor.hostname,
'hours': hours,
'activity': {
'sightings': sightings_count,
'alerts': alerts_count,
'events': events_count,
},
'recent_events': [e.to_dict() for e in recent_events]
}