Skip to content

Commit 7903dbf

Browse files
Replicate lufdetan feinstaub v1 routes (#49)
* /v1/sensors/<sensor_id> all raw measurements of the last 5 minutes for one sensor * Update pytest * /v1/filter?city=&country=&type= 5 minutes filtered by query + Tests * Fix formatting * /static/v2/*.json routes * /static/v2/data.dust|temp.min.json routes * Fix tests + cleanup settings
1 parent 255b49d commit 7903dbf

File tree

12 files changed

+295
-23
lines changed

12 files changed

+295
-23
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,3 +400,4 @@ celerybeat.pid
400400
logs/
401401

402402
/static
403+
/sensorsafrica/static/**/*.json

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ greenlet==0.4.12
1717
whitenoise==4.1.2
1818

1919
pylama==7.6.6
20-
pytest==4.1.1
20+
pytest==5.2.1
2121
pytest-django==3.4.5
2222

2323
ckanapi==4.1

sensorsafrica/api/v1/router.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
SensorDataView,
1010
)
1111

12+
from .views import SensorDataView as SensorsAfricaSensorDataView, FilterView
13+
1214
from rest_framework import routers
1315

1416
router = routers.DefaultRouter()
@@ -19,5 +21,8 @@
1921
router.register(r"statistics", StatisticsView, basename="statistics")
2022
router.register(r"now", NowView)
2123
router.register(r"user", UsersView)
24+
router.register(r"sensors/(?P<sensor_id>\d+)",
25+
SensorsAfricaSensorDataView, basename="sensors")
26+
router.register(r"filter", FilterView, basename="filter")
2227

2328
api_urls = router.urls

sensorsafrica/api/v1/serializers.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from rest_framework import serializers
2+
from feinstaub.sensors.models import (
3+
SensorData,
4+
SensorDataValue
5+
)
6+
7+
8+
class SensorDataValueSerializer(serializers.ModelSerializer):
9+
class Meta:
10+
model = SensorDataValue
11+
fields = ['value_type', 'value']
12+
13+
14+
class SensorDataSerializer(serializers.ModelSerializer):
15+
sensordatavalues = SensorDataValueSerializer(many=True)
16+
17+
class Meta:
18+
model = SensorData
19+
fields = ['timestamp', 'sensordatavalues']

sensorsafrica/api/v1/views.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import datetime
2+
import pytz
3+
import json
4+
5+
from rest_framework.exceptions import ValidationError
6+
7+
from django.conf import settings
8+
from django.utils import timezone
9+
from dateutil.relativedelta import relativedelta
10+
from django.db.models import ExpressionWrapper, F, FloatField, Max, Min, Sum, Avg, Q
11+
from django.db.models.functions import Cast, TruncDate
12+
from rest_framework import mixins, pagination, viewsets
13+
14+
from .serializers import SensorDataSerializer
15+
from feinstaub.sensors.models import SensorData
16+
17+
18+
class SensorDataView(mixins.ListModelMixin, viewsets.GenericViewSet):
19+
serializer_class = SensorDataSerializer
20+
21+
def get_queryset(self):
22+
return (
23+
SensorData.objects
24+
.filter(
25+
timestamp__gte=timezone.now() - datetime.timedelta(minutes=5),
26+
sensor=self.kwargs["sensor_id"]
27+
)
28+
.only('sensor', 'timestamp')
29+
.prefetch_related('sensordatavalues')
30+
)
31+
32+
33+
class FilterView(mixins.ListModelMixin, viewsets.GenericViewSet):
34+
serializer_class = SensorDataSerializer
35+
36+
def get_queryset(self):
37+
sensor_type = self.request.GET.get('type', r'\w+')
38+
country = self.request.GET.get('country', r'\w+')
39+
city = self.request.GET.get('city', r'\w+')
40+
return (
41+
SensorData.objects
42+
.filter(
43+
timestamp__gte=timezone.now() - datetime.timedelta(minutes=5),
44+
sensor__sensor_type__uid__iregex=sensor_type,
45+
location__country__iregex=country,
46+
location__city__iregex=city
47+
)
48+
.only('sensor', 'timestamp')
49+
.prefetch_related('sensordatavalues')
50+
)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
from django.core.management import BaseCommand
2+
from django.core.cache import cache
3+
4+
from django.conf import settings
5+
6+
from django.forms.models import model_to_dict
7+
8+
from feinstaub.sensors.models import SensorLocation, Sensor, SensorType
9+
10+
import os
11+
import json
12+
import datetime
13+
from django.utils import timezone
14+
15+
from django.db import connection
16+
17+
from rest_framework import serializers
18+
19+
20+
class SensorTypeSerializer(serializers.ModelSerializer):
21+
class Meta:
22+
model = SensorType
23+
fields = "__all__"
24+
25+
26+
class SensorSerializer(serializers.ModelSerializer):
27+
sensor_type = SensorTypeSerializer()
28+
29+
class Meta:
30+
model = Sensor
31+
fields = "__all__"
32+
33+
34+
class SensorLocationSerializer(serializers.ModelSerializer):
35+
class Meta:
36+
model = SensorLocation
37+
fields = "__all__"
38+
39+
40+
class Command(BaseCommand):
41+
help = ""
42+
43+
def add_arguments(self, parser):
44+
parser.add_argument('--interval', type=str)
45+
46+
def handle(self, *args, **options):
47+
intervals = {'5m': '5 minutes', '1h': '1 hour', '24h': '24 hours'}
48+
paths = {
49+
'5m': [
50+
'../../../static/v2/data.json',
51+
'../../../static/v2/data.dust.min.json',
52+
'../../../static/v2/data.temp.min.json'
53+
],
54+
'1h': ['../../../static/v2/data.1h.json'],
55+
'24h': ['../../../static/v2/data.24h.json']
56+
}
57+
cursor = connection.cursor()
58+
cursor.execute('''
59+
SELECT sd.sensor_id, sdv.value_type, AVG(CAST(sdv."value" AS FLOAT)) as "value", COUNT("value"), sd.location_id
60+
FROM sensors_sensordata sd
61+
INNER JOIN sensors_sensordatavalue sdv
62+
ON sd.id = sdv.sensordata_id WHERE "timestamp" >= (NOW() - interval %s)
63+
GROUP BY sd.sensor_id, sdv.value_type, sd.location_id
64+
''', [intervals[options['interval']]])
65+
66+
data = {}
67+
while True:
68+
row = cursor.fetchone()
69+
if row is None:
70+
break
71+
72+
if row[0] in data:
73+
data[row[0]]['sensordatavalues'].append(dict({
74+
'samples': row[3],
75+
'value': row[2],
76+
'value_type': row[1]
77+
}))
78+
else:
79+
data[row[0]] = dict({
80+
'location': SensorLocationSerializer(SensorLocation.objects.get(pk=row[4])).data,
81+
'sensor': SensorSerializer(Sensor.objects.get(pk=row[0])).data,
82+
'sensordatavalues': [{
83+
'samples': row[3],
84+
'value': row[2],
85+
'value_type': row[1]
86+
}]
87+
})
88+
89+
for path in paths[options['interval']]:
90+
with open(
91+
os.path.join(os.path.dirname(
92+
os.path.abspath(__file__)), path), 'w'
93+
) as f:
94+
if 'dust' in path:
95+
json.dump(list(filter(
96+
lambda d: d['sensor']['sensor_type']['uid'] == 'sds011', data.values())), f)
97+
elif 'temp' in path:
98+
json.dump(list(filter(
99+
lambda d: d['sensor']['sensor_type']['uid'] == 'dht22', data.values())), f)
100+
else:
101+
json.dump(list(data.values()), f)

sensorsafrica/settings.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@
142142

143143

144144
# Celery Broker
145-
CELERY_BROKER_URL = os.environ.get("SENSORSAFRICA_RABBITMQ_URL", "amqp://sensorsafrica:sensorsafrica@localhost//")
145+
CELERY_BROKER_URL = os.environ.get(
146+
"SENSORSAFRICA_RABBITMQ_URL", "amqp://sensorsafrica:sensorsafrica@localhost//")
146147
CELERY_IGNORE_RESULT = True
147148

148149
CELERY_BEAT_SCHEDULE = {
@@ -158,6 +159,14 @@
158159
"task": "sensorsafrica.tasks.cache_lastactive_nodes_data",
159160
"schedule": crontab(minute="*/5")
160161
},
162+
"cache-static-json-data": {
163+
"task": "sensorsafrica.tasks.cache_static_json_data",
164+
"schedule": crontab(minute="*/5")
165+
},
166+
"cache-static-json-data-1h-24h": {
167+
"task": "sensorsafrica.tasks.cache_static_json_data_1h_24h",
168+
"schedule": crontab(hour="*", minute=0)
169+
},
161170
}
162171

163172

sensorsafrica/static/v2/.gitkeep

Whitespace-only changes.

sensorsafrica/tasks.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,14 @@ def archive_data():
1515
@shared_task
1616
def cache_lastactive_nodes_data():
1717
call_command("cache_lastactive_nodes_data")
18+
19+
20+
@shared_task
21+
def cache_static_json_data():
22+
call_command("cache_static_json_data", interval='5m')
23+
24+
25+
@shared_task
26+
def cache_static_json_data_1h_24h():
27+
call_command("cache_static_json_data", interval='1h')
28+
call_command("cache_static_json_data", interval='24h')

sensorsafrica/tests/conftest.py

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ def location():
3737

3838
@pytest.fixture
3939
def sensor_type():
40-
st, x = SensorType.objects.get_or_create(uid="a", name="b", manufacturer="c")
40+
st, x = SensorType.objects.get_or_create(
41+
uid="a", name="b", manufacturer="c")
4142
return st
4243

4344

@@ -58,38 +59,53 @@ def sensor(logged_in_user, sensor_type, node):
5859
@pytest.fixture
5960
def locations():
6061
return [
61-
SensorLocation.objects.get_or_create(city="Dar es Salaam", description="active")[0],
62-
SensorLocation.objects.get_or_create(city="Bagamoyo", description="inactive")[0],
63-
SensorLocation.objects.get_or_create(city="Mombasa", description="inactive")[0],
64-
SensorLocation.objects.get_or_create(city="Nairobi", description="inactive")[0],
65-
SensorLocation.objects.get_or_create(city="Dar es Salaam", description="active | some other node location")[0],
62+
SensorLocation.objects.get_or_create(
63+
city="Dar es Salaam", country="Tanzania", description="active")[0],
64+
SensorLocation.objects.get_or_create(
65+
city="Bagamoyo", country="Tanzania", description="inactive")[0],
66+
SensorLocation.objects.get_or_create(
67+
city="Mombasa", country="Kenya", description="inactive")[0],
68+
SensorLocation.objects.get_or_create(
69+
city="Nairobi", country="Kenya", description="inactive")[0],
70+
SensorLocation.objects.get_or_create(
71+
city="Dar es Salaam", country="Tanzania", description="active | some other node location")[0],
6672
]
6773

6874

6975
@pytest.fixture
7076
def nodes(logged_in_user, locations):
7177
return [
72-
Node.objects.get_or_create(uid="0", owner=logged_in_user, location=locations[0])[0],
73-
Node.objects.get_or_create(uid="1", owner=logged_in_user, location=locations[1])[0],
74-
Node.objects.get_or_create(uid="2", owner=logged_in_user, location=locations[2])[0],
75-
Node.objects.get_or_create(uid="3", owner=logged_in_user, location=locations[3])[0],
76-
Node.objects.get_or_create(uid="4", owner=logged_in_user, location=locations[4])[0],
78+
Node.objects.get_or_create(
79+
uid="0", owner=logged_in_user, location=locations[0])[0],
80+
Node.objects.get_or_create(
81+
uid="1", owner=logged_in_user, location=locations[1])[0],
82+
Node.objects.get_or_create(
83+
uid="2", owner=logged_in_user, location=locations[2])[0],
84+
Node.objects.get_or_create(
85+
uid="3", owner=logged_in_user, location=locations[3])[0],
86+
Node.objects.get_or_create(
87+
uid="4", owner=logged_in_user, location=locations[4])[0],
7788
]
7889

7990

8091
@pytest.fixture
8192
def sensors(sensor_type, nodes):
8293
return [
8394
# Active Dar Sensor
84-
Sensor.objects.get_or_create(node=nodes[0], sensor_type=sensor_type, public=True)[0],
95+
Sensor.objects.get_or_create(
96+
node=nodes[0], sensor_type=sensor_type, public=True)[0],
8597
# Inactive with last data push beyond active threshold
86-
Sensor.objects.get_or_create(node=nodes[1], sensor_type=sensor_type)[0],
98+
Sensor.objects.get_or_create(
99+
node=nodes[1], sensor_type=sensor_type)[0],
87100
# Inactive without any data
88-
Sensor.objects.get_or_create(node=nodes[2], sensor_type=sensor_type)[0],
101+
Sensor.objects.get_or_create(
102+
node=nodes[2], sensor_type=sensor_type)[0],
89103
# Active Nairobi Sensor
90-
Sensor.objects.get_or_create(node=nodes[3], sensor_type=sensor_type)[0],
104+
Sensor.objects.get_or_create(
105+
node=nodes[3], sensor_type=sensor_type)[0],
91106
# Active Dar Sensor another location
92-
Sensor.objects.get_or_create(node=nodes[4], sensor_type=sensor_type)[0],
107+
Sensor.objects.get_or_create(
108+
node=nodes[4], sensor_type=sensor_type)[0],
93109
]
94110

95111

@@ -111,11 +127,13 @@ def sensordata(sensors, locations):
111127
]
112128
# Nairobi SensorData
113129
for i in range(100):
114-
sensor_datas.append(SensorData(sensor=sensors[3], location=locations[3]))
130+
sensor_datas.append(SensorData(
131+
sensor=sensors[3], location=locations[3]))
115132

116133
# Dar es Salaam another node location SensorData
117134
for i in range(6):
118-
sensor_datas.append(SensorData(sensor=sensors[4], location=locations[4]))
135+
sensor_datas.append(SensorData(
136+
sensor=sensors[4], location=locations[4]))
119137

120138
data = SensorData.objects.bulk_create(sensor_datas)
121139

@@ -130,7 +148,8 @@ def sensordata(sensors, locations):
130148
def datavalues(sensors, sensordata):
131149
data_values = [
132150
# Bagamoyo
133-
SensorDataValue(sensordata=sensordata[0], value="2", value_type="humidity"),
151+
SensorDataValue(
152+
sensordata=sensordata[0], value="2", value_type="humidity"),
134153
# Dar es salaam a day ago's data
135154
SensorDataValue(sensordata=sensordata[1], value="1", value_type="P2"),
136155
SensorDataValue(sensordata=sensordata[2], value="2", value_type="P2"),
@@ -150,12 +169,14 @@ def datavalues(sensors, sensordata):
150169
# Nairobi SensorDataValues
151170
for i in range(100):
152171
data_values.append(
153-
SensorDataValue(sensordata=sensordata[9 + i], value="0", value_type="P2")
172+
SensorDataValue(
173+
sensordata=sensordata[9 + i], value="0", value_type="P2")
154174
)
155175

156176
# Dar es Salaam another node location SensorDataValues
157177
for i in range(6):
158-
data_values.append(SensorDataValue(sensordata=sensordata[109 + i], value="0.0", value_type="P2"))
178+
data_values.append(SensorDataValue(
179+
sensordata=sensordata[109 + i], value="0.0", value_type="P2"))
159180

160181
values = SensorDataValue.objects.bulk_create(data_values)
161182

0 commit comments

Comments
 (0)