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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user