Skip to content

Commit aa7397c

Browse files
authored
Merge pull request #78 from luftdaten-at/75-api-for-aircube
75 api for aircube
2 parents af60407 + 9526fd8 commit aa7397c

File tree

12 files changed

+210
-11
lines changed

12 files changed

+210
-11
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 5.1.2 on 2024-12-20 12:48
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('api', '0004_alter_devicelogs_id_alter_measurementnew_id_and_more'),
11+
('campaign', '0008_alter_organizationinvitation_expiring_date'),
12+
]
13+
14+
operations = [
15+
migrations.AlterField(
16+
model_name='measurementnew',
17+
name='room',
18+
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='campaign.room'),
19+
),
20+
]

app/api/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ class MeasurementNew(models.Model):
8686
time_measured = models.DateTimeField()
8787
sensor_model = models.IntegerField()
8888
device = models.ForeignKey(Device, on_delete=models.CASCADE)
89-
room = models.ForeignKey(Room, on_delete=models.CASCADE)
89+
room = models.ForeignKey(Room, on_delete=models.CASCADE, null=True)
9090

9191
def __str__(self):
9292
return f'Measurement {self.id} from Device {self.device.id}'

app/api/urls.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from django.urls import include, path
2-
from .views import AirQualityDataAddView, DeviceDetailView, DeviceDataAddView, WorkshopDetailView, WorkshopAirQualityDataView
32
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
43

54
urlpatterns = [

app/api/urls_v1.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from django.urls import path
2-
from .views import AirQualityDataAddView, DeviceDetailView, DeviceDataAddView, DeviceStatusView, WorkshopDetailView, WorkshopAirQualityDataView
2+
from .views import CreateStationStatusAPIView, AirQualityDataAddView, DeviceDetailView, DeviceDataAddView, DeviceStatusView, WorkshopDetailView, WorkshopAirQualityDataView, CreateStationDataAPIView
33

44
urlpatterns = [
55
path('devices/<str:pk>/', DeviceDetailView.as_view(), name='api-v1-device-detail'),
@@ -8,4 +8,6 @@
88
path('workshops/<str:pk>/data/', WorkshopAirQualityDataView.as_view(), name='api-v1-workshop-air-quality-data'),
99
path('workshops/data/add/', AirQualityDataAddView.as_view(), name='api-v1-air-quality-data-add'),
1010
path('workshops/<str:pk>/', WorkshopDetailView.as_view(), name='api-v1-workshop-detail'),
11+
path('status/', CreateStationStatusAPIView.as_view(), name='station-status'),
12+
path('data/', CreateStationDataAPIView.as_view(), name='station-data'),
1113
]

app/api/views.py

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
from django.db import IntegrityError
1+
import datetime
2+
from django.db import IntegrityError, transaction
23
from django.utils.dateparse import parse_datetime
34

45
from rest_framework.views import APIView
56
from rest_framework.response import Response
67
from rest_framework import status
78
from rest_framework.generics import RetrieveAPIView
9+
from rest_framework.exceptions import ValidationError
10+
from django.http import JsonResponse
811

9-
from .models import AirQualityRecord, AirQualityDatapoint, MobilityMode, Measurement
12+
from main.util import get_or_create_station
13+
from .models import AirQualityRecord, AirQualityDatapoint, MobilityMode, Measurement, DeviceLogs, Values, MeasurementNew
1014
from workshops.models import Participant, Workshop
1115
from devices.models import Device
1216

@@ -128,4 +132,103 @@ class WorkshopAirQualityDataView(RetrieveAPIView):
128132
def get(self, request, pk):
129133
records = AirQualityRecord.objects.filter(workshop__name=pk)
130134
serializer = AirQualityRecordSerializer(records, many=True)
131-
return Response(serializer.data)
135+
return Response(serializer.data)
136+
137+
138+
class CreateStationStatusAPIView(APIView):
139+
def post(self, request, *args, **kwargs):
140+
print(request)
141+
station_data = request.data.get('station')
142+
status_list = request.data.get('status_list', [])
143+
144+
if not station_data or not status_list:
145+
raise ValidationError("Both 'station' and 'status_list' are required.")
146+
147+
# Get or create the station
148+
station = get_or_create_station(station_info=station_data)
149+
150+
if station.api_key != station_data.get('apikey'):
151+
raise ValidationError("Wrong API Key")
152+
153+
try:
154+
with transaction.atomic():
155+
for status_data in status_list:
156+
# Manually create and save the DeviceLogs object
157+
DeviceLogs.objects.create(
158+
device=station,
159+
timestamp=status_data['time'],
160+
level=status_data.get('level', 1), # Default level 1 if not provided
161+
message=status_data.get('message', ''), # Default empty message if not provided
162+
)
163+
164+
return Response({"status": "success"}, status=200)
165+
166+
except Exception as e:
167+
return Response({"status": "error", "message": str(e)}, status=status.HTTP_400_BAD_REQUEST)
168+
169+
170+
class CreateStationDataAPIView(APIView):
171+
def post(self, request, *args, **kwargs):
172+
print(request)
173+
# Parse the incoming JSON data
174+
try:
175+
station_data = request.data.get('station')
176+
sensors_data = request.data.get('sensors')
177+
178+
if not station_data or not sensors_data:
179+
raise ValidationError("Both 'station' and 'sensors' are required.")
180+
181+
# Use the get_or_create_station function to get or create the station
182+
station = get_or_create_station(station_data)
183+
184+
if station.api_key != station_data.get('apikey'):
185+
raise ValidationError("Wrong API Key")
186+
187+
# Record the time when the request was received
188+
time_received = datetime.datetime.now(datetime.timezone.utc)
189+
190+
try:
191+
with transaction.atomic():
192+
# Iterate through all sensors
193+
for sensor_id, sensor_data in sensors_data.items():
194+
# Check if the measurement already exists in the database
195+
existing_measurement = MeasurementNew.objects.filter(
196+
device=station,
197+
time_measured=station_data['time'],
198+
sensor_model=sensor_data['type']
199+
).first()
200+
201+
if existing_measurement:
202+
return JsonResponse(
203+
{"status": "error", "detail": "Measurement already in Database"},
204+
status=422
205+
)
206+
207+
# If no existing measurement, create a new one
208+
measurement = MeasurementNew(
209+
sensor_model=sensor_data['type'],
210+
device=station,
211+
time_measured=station_data['time'],
212+
time_received=time_received,
213+
)
214+
measurement.save()
215+
216+
# Add values (dimension, value) for the measurement
217+
for dimension, value in sensor_data['data'].items():
218+
Values.objects.create(
219+
dimension=dimension,
220+
value=value,
221+
measurement=measurement
222+
)
223+
224+
# Update the station's last active time
225+
station.last_update = station_data['time']
226+
station.save()
227+
228+
return JsonResponse({"status": "success"}, status=200)
229+
230+
except Exception as e:
231+
return JsonResponse({"status": "error", "message": str(e)}, status=400)
232+
233+
except Exception as e:
234+
return JsonResponse({"status": "error", "message": str(e)}, status=400)

app/campaign/forms.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ def save(self, commit=True):
5252
# Set the `public` field to False
5353
campaign.public = False
5454
campaign.owner = self.user
55-
campaign.users.add(self.user)
5655
campaign.organization = self.user.organizations.first()
56+
campaign.save()
57+
campaign.users.add(self.user)
5758

5859
# Save to the database if commit is True
59-
if commit:
60-
campaign.save()
60+
campaign.save()
6161

6262
return campaign
6363

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.1.2 on 2024-12-20 11:35
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('devices', '0005_alter_sensor_id'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='sensor',
15+
name='api_key',
16+
field=models.CharField(max_length=64, null=True),
17+
),
18+
]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated by Django 5.1.2 on 2024-12-20 11:43
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('devices', '0006_sensor_api_key'),
10+
]
11+
12+
operations = [
13+
migrations.RemoveField(
14+
model_name='sensor',
15+
name='api_key',
16+
),
17+
migrations.AddField(
18+
model_name='device',
19+
name='api_key',
20+
field=models.CharField(max_length=64, null=True),
21+
),
22+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.1.2 on 2025-01-02 10:10
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('devices', '0007_remove_sensor_api_key_device_api_key'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='device',
15+
name='firmware',
16+
field=models.CharField(blank=True, max_length=255),
17+
),
18+
]

app/devices/models.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ class Device(models.Model):
99
id = models.CharField(max_length=255, primary_key=True)
1010
device_name = models.CharField(max_length=255, blank=True, null=True)
1111
model = models.CharField(max_length=255, blank=True, null=True)
12-
firmware = models.CharField(max_length=12, blank=True)
12+
firmware = models.CharField(max_length=255, blank=True)
1313
btmac_address = models.CharField(max_length=12, null=True, blank=True)
1414
last_update = models.DateTimeField(null=True, blank=True)
1515
notes = models.TextField(null=True, blank=True)
16+
api_key = models.CharField(max_length=64, null=True)
1617

1718
def __str__(self):
1819
return self.id or "Undefined Device" # Added fallback for undefined IDs

app/main/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"staging.arbeitsplatz.luftdaten.at",
3838
"localhost",
3939
"127.0.0.1",
40-
"172.18.0.*"
40+
"172.18.0.*",
4141
]
4242

4343

app/main/util.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import datetime
2+
3+
from api.models import Device
4+
5+
def get_or_create_station(station_info: dict):
6+
station, created = Device.objects.get_or_create(
7+
id = station_info['device']
8+
)
9+
if created:
10+
station.device_name = station_info['device']
11+
station.firmware = station_info['firmware']
12+
station.last_update = datetime.datetime.now(datetime.timezone.utc)
13+
station.api_key = station_info['apikey']
14+
station.save()
15+
16+
return station

0 commit comments

Comments
 (0)