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,7 +1,7 @@
"""Sensor API tests."""
from unittest.mock import patch, MagicMock
from esp32_web.extensions import db
from esp32_web.models import Sensor
from esp32_web.models import Sensor, Event
def test_list_sensors_empty(client):
@@ -215,3 +215,123 @@ def test_trigger_calibrate_default_seconds(client, app):
json={})
assert response.status_code == 200
assert response.json['seconds'] == 10
# Heartbeat Tests
def test_heartbeat_status_empty(client):
"""Test heartbeat status with no sensors."""
response = client.get('/api/v1/sensors/heartbeat')
assert response.status_code == 200
assert response.json['total'] == 0
assert response.json['sensors'] == []
def test_heartbeat_status_with_sensors(client, app):
"""Test heartbeat status with sensors."""
from datetime import datetime, UTC, timedelta
with app.app_context():
# Online sensor (just now)
s1 = Sensor(hostname='sensor-online', ip='192.168.1.1',
last_seen=datetime.now(UTC))
# Stale sensor (3 minutes ago)
s2 = Sensor(hostname='sensor-stale', ip='192.168.1.2',
last_seen=datetime.now(UTC) - timedelta(minutes=3))
# Offline sensor (10 minutes ago)
s3 = Sensor(hostname='sensor-offline', ip='192.168.1.3',
last_seen=datetime.now(UTC) - timedelta(minutes=10))
db.session.add_all([s1, s2, s3])
db.session.commit()
response = client.get('/api/v1/sensors/heartbeat')
assert response.status_code == 200
assert response.json['total'] == 3
assert response.json['online'] == 1
assert response.json['stale'] == 1
assert response.json['offline'] == 1
def test_refresh_heartbeats(client, app):
"""Test refreshing heartbeat status."""
from datetime import datetime, UTC, timedelta
with app.app_context():
# Offline sensor but status still says 'online'
sensor = Sensor(hostname='test-sensor', ip='192.168.1.1',
last_seen=datetime.now(UTC) - timedelta(minutes=10),
status='online')
db.session.add(sensor)
db.session.commit()
response = client.post('/api/v1/sensors/heartbeat')
assert response.status_code == 200
assert response.json['status'] == 'updated'
assert response.json['offline'] == 1
# Verify status was updated
with app.app_context():
sensor = db.session.scalar(db.select(Sensor).where(Sensor.hostname == 'test-sensor'))
assert sensor.status == 'offline'
# Metrics Tests
def test_metrics_not_found(client):
"""Test metrics for non-existent sensor."""
response = client.get('/api/v1/sensors/nonexistent/metrics')
assert response.status_code == 404
def test_metrics_empty(client, app):
"""Test metrics for sensor with no activity."""
with app.app_context():
sensor = Sensor(hostname='test-sensor', ip='192.168.1.100')
db.session.add(sensor)
db.session.commit()
response = client.get('/api/v1/sensors/test-sensor/metrics')
assert response.status_code == 200
assert response.json['hostname'] == 'test-sensor'
assert response.json['hours'] == 24
assert response.json['activity']['sightings'] == 0
assert response.json['activity']['alerts'] == 0
assert response.json['activity']['events'] == 0
assert response.json['recent_events'] == []
def test_metrics_with_events(client, app):
"""Test metrics with sensor events."""
from datetime import datetime, UTC
with app.app_context():
sensor = Sensor(hostname='test-sensor', ip='192.168.1.100')
db.session.add(sensor)
db.session.flush()
# Add some events
event1 = Event(sensor_id=sensor.id, event_type='presence',
payload_json='{"state": "detected"}',
timestamp=datetime.now(UTC))
event2 = Event(sensor_id=sensor.id, event_type='calibration',
payload_json='{"nsub": 52}',
timestamp=datetime.now(UTC))
db.session.add_all([event1, event2])
db.session.commit()
response = client.get('/api/v1/sensors/test-sensor/metrics')
assert response.status_code == 200
assert response.json['activity']['events'] == 2
assert len(response.json['recent_events']) == 2
def test_metrics_custom_hours(client, app):
"""Test metrics with custom time range."""
with app.app_context():
sensor = Sensor(hostname='test-sensor', ip='192.168.1.100')
db.session.add(sensor)
db.session.commit()
response = client.get('/api/v1/sensors/test-sensor/metrics?hours=48')
assert response.status_code == 200
assert response.json['hours'] == 48