Skip to content

Commit c3a42dd

Browse files
committed
Added aggregated stats to device api endpoint
1 parent 8db707f commit c3a42dd

File tree

7 files changed

+106
-14
lines changed

7 files changed

+106
-14
lines changed

iotserver/apps/device/api/serializers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class DeviceListDetailSerializer(serializers.ModelSerializer):
4949
location = LocationSerializer(many=False, read_only=True)
5050
pins = DevicePinSerializer(many=True, read_only=True)
5151
last_status = serializers.SerializerMethodField()
52+
aggregated_status = serializers.SerializerMethodField()
5253
health = serializers.SerializerMethodField()
5354

5455
class Meta:
@@ -58,7 +59,11 @@ class Meta:
5859

5960
def get_last_status(self, instance):
6061
if instance.last_status:
61-
return DeviceStatusSerializer(instance.last_status).data
62+
return DeviceStatusSerializer(instance.last_status['status']).data
63+
64+
def get_aggregated_status(self, instance):
65+
if instance.last_status:
66+
return instance.last_status['aggregates']
6267

6368
def get_health(self, instance):
6469
try:

iotserver/apps/device/models.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from django.utils.functional import cached_property
1212

1313
from iotserver.apps.device import constants
14-
from iotserver.apps.device.utils import mqtt, webrepl
14+
from iotserver.apps.device.utils import mqtt, stats, webrepl
1515

1616

1717
class Location(models.Model):
@@ -89,7 +89,10 @@ def full_config(self):
8989

9090
@cached_property
9191
def last_status(self):
92-
return self.statuses.first()
92+
return {
93+
'status': self.statuses.first(),
94+
'aggregates': stats.aggregate_statuses(self.statuses),
95+
}
9396

9497
def mqtt_toggle(self, state: str):
9598
mqtt.toggle(self.id, str(constants.DEVICE_TOGGLE_STATE[state]))

iotserver/apps/device/tests/test_models.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,39 @@ class TestDeviceModel(object):
3838
def setup_method(self, test_method):
3939
self.device = device_factories.DeviceFactory()
4040
self.device_pin = device_factories.DevicePinFactory(devices=[self.device])
41-
self.device_status = device_factories.DeviceStatusFactory(device=self.device)
41+
self.device_statuses = [
42+
device_factories.DeviceStatusFactory(
43+
device=self.device,
44+
status={
45+
'dht-sensor': {
46+
'humidity': 10,
47+
'temperature': 10
48+
},
49+
'light-sensor': 10
50+
},
51+
),
52+
device_factories.DeviceStatusFactory(
53+
device=self.device,
54+
status={
55+
'dht-sensor': {
56+
'humidity': 20,
57+
'temperature': 20
58+
},
59+
'light-sensor': 20
60+
},
61+
),
62+
device_factories.DeviceStatusFactory(
63+
device=self.device,
64+
status={
65+
'dht-sensor': {
66+
'humidity': 12,
67+
'temperature': 12
68+
},
69+
'light-sensor': 12
70+
},
71+
)
72+
]
73+
self.device_status = self.device_statuses[2]
4274

4375
def test_str(self):
4476
assert str(self.device) == self.device.name
@@ -53,7 +85,14 @@ def test_full_config(self):
5385
}
5486

5587
def test_last_status(self):
56-
assert self.device.last_status.pk == self.device_status.pk
88+
assert self.device.last_status['status'].pk == self.device_status.pk
89+
assert self.device.last_status['aggregates'] == {
90+
'dht-sensor': {
91+
'humidity': {'minimum': 10, 'maximum': 20, 'average': 15},
92+
'temperature': {'minimum': 10, 'maximum': 20, 'average': 15}
93+
},
94+
'light-sensor': {'minimum': 10, 'maximum': 20, 'average': 15}
95+
}
5796

5897
def test_mqtt_toggle(self, mocker):
5998
mock_mqtt_toggle = mocker.patch('iotserver.apps.device.models.mqtt.toggle')
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from django.utils import timezone
2+
3+
4+
def aggregate_statuses(statuses):
5+
"""
6+
Returns device stats by minimum, maximum and average values for today.
7+
"""
8+
today = timezone.now().date()
9+
statuses = statuses.filter(created_at__date=today)
10+
11+
aggregates = {}
12+
for status in statuses:
13+
for key, value in status.status.items():
14+
if isinstance(value, dict):
15+
if key not in aggregates:
16+
aggregates[key] = {}
17+
for subkey, subvalue in value.items():
18+
if subkey not in aggregates[key]:
19+
aggregates[key][subkey] = {
20+
'minimum': subvalue,
21+
'maximum': subvalue
22+
}
23+
else:
24+
aggregates[key][subkey]['minimum'] = min(
25+
aggregates[key][subkey]['minimum'], subvalue
26+
)
27+
aggregates[key][subkey]['maximum'] = max(
28+
aggregates[key][subkey]['maximum'], subvalue
29+
)
30+
aggregates[key][subkey]['average'] = (
31+
aggregates[key][subkey]['minimum'] +
32+
aggregates[key][subkey]['maximum']
33+
) / 2
34+
else:
35+
if key not in aggregates:
36+
aggregates[key] = {'minimum': value, 'maximum': value}
37+
else:
38+
aggregates[key]['minimum'] = min(aggregates[key]['minimum'], value)
39+
aggregates[key]['maximum'] = max(aggregates[key]['maximum'], value)
40+
aggregates[key]['average'] = (
41+
aggregates[key]['minimum'] + aggregates[key]['maximum']
42+
) / 2
43+
return aggregates

iotserver/tests/test_views.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ def test_detail__no_device(self, api_client):
2525

2626
def test_detail__device_id(self, api_client):
2727
response = api_client.get(f'{self.root_url}{self.device_health.device_id}/')
28+
2829
return_data = response.json()
2930

3031
assert response.status_code == 200
3132
assert return_data['status'] == 'ok'
33+
34+
assert self.device_health.updated_at != self.device_health.created_at

iotserver/urls.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
# Health endpoint
3131
urlpatterns = [
32+
path('health/<str:device_pk>/', health),
3233
path('health/', health),
3334
]
3435

@@ -51,14 +52,5 @@
5152
path('api/schema/docs/', SpectacularRedocView.as_view(url_name='schema'), name='docs'),
5253
]
5354

54-
# from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
55-
# urlpatterns = [
56-
# # YOUR PATTERNS
57-
# path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
58-
# # Optional UI:
59-
# path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
60-
# path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
61-
# ]
62-
6355
# Static files
6456
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

iotserver/views.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,16 @@
55
)
66
from rest_framework.response import Response
77

8+
from iotserver.apps.device.models import DeviceHealth
9+
810

911
@api_view(['GET'])
1012
@authentication_classes([])
1113
@permission_classes([])
1214
def health(request, device_pk=None):
15+
if device_pk is not None:
16+
device_health, created = DeviceHealth.objects.get_or_create(device_id=device_pk)
17+
if not created:
18+
device_health.save()
19+
1320
return Response({'status': 'ok'})

0 commit comments

Comments
 (0)