From 4135261f203d9ec4559e0b908e6dba64d08d5269 Mon Sep 17 00:00:00 2001 From: Nik Sauer Date: Fri, 20 Dec 2024 12:14:52 +0100 Subject: [PATCH 01/10] added api view for station status --- app/api/urls.py | 1 - app/api/urls_v1.py | 3 ++- app/api/views.py | 35 ++++++++++++++++++++++++++++++++--- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/app/api/urls.py b/app/api/urls.py index 8c01bd6..516ad40 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -1,5 +1,4 @@ from django.urls import include, path -from .views import AirQualityDataAddView, DeviceDetailView, DeviceDataAddView, WorkshopDetailView, WorkshopAirQualityDataView from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView urlpatterns = [ diff --git a/app/api/urls_v1.py b/app/api/urls_v1.py index 8a1befe..00b846f 100644 --- a/app/api/urls_v1.py +++ b/app/api/urls_v1.py @@ -1,5 +1,5 @@ from django.urls import path -from .views import AirQualityDataAddView, DeviceDetailView, DeviceDataAddView, DeviceStatusView, WorkshopDetailView, WorkshopAirQualityDataView +from .views import CreateStationStatusAPIView, AirQualityDataAddView, DeviceDetailView, DeviceDataAddView, DeviceStatusView, WorkshopDetailView, WorkshopAirQualityDataView urlpatterns = [ path('devices//', DeviceDetailView.as_view(), name='api-v1-device-detail'), @@ -8,4 +8,5 @@ path('workshops//data/', WorkshopAirQualityDataView.as_view(), name='api-v1-workshop-air-quality-data'), path('workshops/data/add/', AirQualityDataAddView.as_view(), name='api-v1-air-quality-data-add'), path('workshops//', WorkshopDetailView.as_view(), name='api-v1-workshop-detail'), + path('status/', CreateStationStatusAPIView.as_view(), name='station-status'), ] \ No newline at end of file diff --git a/app/api/views.py b/app/api/views.py index 7395a4e..ce80db9 100644 --- a/app/api/views.py +++ b/app/api/views.py @@ -1,12 +1,13 @@ -from django.db import IntegrityError +from django.db import IntegrityError, transaction from django.utils.dateparse import parse_datetime from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from rest_framework.generics import RetrieveAPIView +from rest_framework.exceptions import ValidationError -from .models import AirQualityRecord, AirQualityDatapoint, MobilityMode, Measurement +from .models import AirQualityRecord, AirQualityDatapoint, MobilityMode, Measurement, DeviceLogs from workshops.models import Participant, Workshop from devices.models import Device @@ -128,4 +129,32 @@ class WorkshopAirQualityDataView(RetrieveAPIView): def get(self, request, pk): records = AirQualityRecord.objects.filter(workshop__name=pk) serializer = AirQualityRecordSerializer(records, many=True) - return Response(serializer.data) \ No newline at end of file + return Response(serializer.data) + + +class CreateStationStatusAPIView(APIView): + def post(self, request, *args, **kwargs): + station_data = request.data.get('station') + status_list = request.data.get('status_list', []) + + if not station_data or not status_list: + raise ValidationError("Both 'station' and 'status_list' are required.") + + # Get or create the station + station, created = Device.objects.get_or_create(id=station_data['id']) + + try: + with transaction.atomic(): + for status_data in status_list: + # Manually create and save the DeviceLogs object + DeviceLogs.objects.create( + device=station, + timestamp=status_data['time'], + level=status_data.get('level', 1), # Default level 1 if not provided + message=status_data.get('message', '') # Default empty message if not provided + ) + + return Response({"status": "success"}, status=status.HTTP_201_CREATED) + + except Exception as e: + return Response({"status": "error", "message": str(e)}, status=status.HTTP_400_BAD_REQUEST) From d922ae71d9a9edd6839a818bf59135fb59550641 Mon Sep 17 00:00:00 2001 From: Nik Sauer Date: Fri, 20 Dec 2024 12:31:25 +0100 Subject: [PATCH 02/10] fixed campaing create --- app/api/views.py | 2 +- app/campaign/forms.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/api/views.py b/app/api/views.py index ce80db9..6fa377d 100644 --- a/app/api/views.py +++ b/app/api/views.py @@ -141,7 +141,7 @@ def post(self, request, *args, **kwargs): raise ValidationError("Both 'station' and 'status_list' are required.") # Get or create the station - station, created = Device.objects.get_or_create(id=station_data['id']) + station, created = Device.objects.get_or_create(device_name=station_data['device']) try: with transaction.atomic(): diff --git a/app/campaign/forms.py b/app/campaign/forms.py index 0589b4b..aa73213 100644 --- a/app/campaign/forms.py +++ b/app/campaign/forms.py @@ -52,12 +52,12 @@ def save(self, commit=True): # Set the `public` field to False campaign.public = False campaign.owner = self.user - campaign.users.add(self.user) campaign.organization = self.user.organizations.first() + campaign.save() + campaign.users.add(self.user) # Save to the database if commit is True - if commit: - campaign.save() + campaign.save() return campaign From f8af1521b68e63b1022df6e9d75296a3ddc245d6 Mon Sep 17 00:00:00 2001 From: Nik Sauer Date: Fri, 20 Dec 2024 12:36:44 +0100 Subject: [PATCH 03/10] added api_key migrations --- app/api/views.py | 2 ++ app/devices/migrations/0006_sensor_api_key.py | 18 ++++++++++++++++++ app/devices/models.py | 1 + 3 files changed, 21 insertions(+) create mode 100644 app/devices/migrations/0006_sensor_api_key.py diff --git a/app/api/views.py b/app/api/views.py index 6fa377d..700e7eb 100644 --- a/app/api/views.py +++ b/app/api/views.py @@ -158,3 +158,5 @@ def post(self, request, *args, **kwargs): except Exception as e: return Response({"status": "error", "message": str(e)}, status=status.HTTP_400_BAD_REQUEST) + + diff --git a/app/devices/migrations/0006_sensor_api_key.py b/app/devices/migrations/0006_sensor_api_key.py new file mode 100644 index 0000000..a676eb8 --- /dev/null +++ b/app/devices/migrations/0006_sensor_api_key.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2024-12-20 11:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('devices', '0005_alter_sensor_id'), + ] + + operations = [ + migrations.AddField( + model_name='sensor', + name='api_key', + field=models.CharField(max_length=64, null=True), + ), + ] diff --git a/app/devices/models.py b/app/devices/models.py index 8b20ed9..ac1af2e 100644 --- a/app/devices/models.py +++ b/app/devices/models.py @@ -29,6 +29,7 @@ class Sensor(models.Model): firmware = models.CharField(max_length=50, blank=True) # Für die Firmware-Version hardware = models.CharField(max_length=50, blank=True) # Für die Hardware-Version protocol = models.CharField(max_length=50, blank=True) # Für die Protokoll-Version + api_key = models.CharField(max_length=64, null=True) def __str__(self): return self.name From bafe5565645b5d055b4015361b22fb0937c8353b Mon Sep 17 00:00:00 2001 From: Nik Sauer Date: Fri, 20 Dec 2024 12:43:59 +0100 Subject: [PATCH 04/10] fixed migrations --- app/api/views.py | 2 ++ ...07_remove_sensor_api_key_device_api_key.py | 22 +++++++++++++++++++ app/devices/models.py | 2 +- app/main/util.py | 0 4 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 app/devices/migrations/0007_remove_sensor_api_key_device_api_key.py create mode 100644 app/main/util.py diff --git a/app/api/views.py b/app/api/views.py index 700e7eb..b9343bb 100644 --- a/app/api/views.py +++ b/app/api/views.py @@ -143,6 +143,8 @@ def post(self, request, *args, **kwargs): # Get or create the station station, created = Device.objects.get_or_create(device_name=station_data['device']) + #if not created and station.api_key != + try: with transaction.atomic(): for status_data in status_list: diff --git a/app/devices/migrations/0007_remove_sensor_api_key_device_api_key.py b/app/devices/migrations/0007_remove_sensor_api_key_device_api_key.py new file mode 100644 index 0000000..ac31c93 --- /dev/null +++ b/app/devices/migrations/0007_remove_sensor_api_key_device_api_key.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.2 on 2024-12-20 11:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('devices', '0006_sensor_api_key'), + ] + + operations = [ + migrations.RemoveField( + model_name='sensor', + name='api_key', + ), + migrations.AddField( + model_name='device', + name='api_key', + field=models.CharField(max_length=64, null=True), + ), + ] diff --git a/app/devices/models.py b/app/devices/models.py index ac1af2e..8e93079 100644 --- a/app/devices/models.py +++ b/app/devices/models.py @@ -13,6 +13,7 @@ class Device(models.Model): btmac_address = models.CharField(max_length=12, null=True, blank=True) last_update = models.DateTimeField(null=True, blank=True) notes = models.TextField(null=True, blank=True) + api_key = models.CharField(max_length=64, null=True) def __str__(self): return self.id or "Undefined Device" # Added fallback for undefined IDs @@ -29,7 +30,6 @@ class Sensor(models.Model): firmware = models.CharField(max_length=50, blank=True) # Für die Firmware-Version hardware = models.CharField(max_length=50, blank=True) # Für die Hardware-Version protocol = models.CharField(max_length=50, blank=True) # Für die Protokoll-Version - api_key = models.CharField(max_length=64, null=True) def __str__(self): return self.name diff --git a/app/main/util.py b/app/main/util.py new file mode 100644 index 0000000..e69de29 From 54746d202937b503a485c174f63e5e56eff6aff3 Mon Sep 17 00:00:00 2001 From: Nik Sauer Date: Fri, 20 Dec 2024 12:57:38 +0100 Subject: [PATCH 05/10] added get_or_create_station --- app/api/views.py | 10 +++++----- app/main/util.py | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/app/api/views.py b/app/api/views.py index b9343bb..7e93378 100644 --- a/app/api/views.py +++ b/app/api/views.py @@ -7,6 +7,7 @@ from rest_framework.generics import RetrieveAPIView from rest_framework.exceptions import ValidationError +from main.util import get_or_create_station from .models import AirQualityRecord, AirQualityDatapoint, MobilityMode, Measurement, DeviceLogs from workshops.models import Participant, Workshop from devices.models import Device @@ -141,9 +142,10 @@ def post(self, request, *args, **kwargs): raise ValidationError("Both 'station' and 'status_list' are required.") # Get or create the station - station, created = Device.objects.get_or_create(device_name=station_data['device']) + station = get_or_create_station(station_info=station_data) - #if not created and station.api_key != + if station.api_key != station_data.get('apikey'): + raise ValidationError("Wrong API Key") try: with transaction.atomic(): @@ -153,12 +155,10 @@ def post(self, request, *args, **kwargs): device=station, timestamp=status_data['time'], level=status_data.get('level', 1), # Default level 1 if not provided - message=status_data.get('message', '') # Default empty message if not provided + message=status_data.get('message', ''), # Default empty message if not provided ) return Response({"status": "success"}, status=status.HTTP_201_CREATED) except Exception as e: return Response({"status": "error", "message": str(e)}, status=status.HTTP_400_BAD_REQUEST) - - diff --git a/app/main/util.py b/app/main/util.py index e69de29..bc4f7aa 100644 --- a/app/main/util.py +++ b/app/main/util.py @@ -0,0 +1,16 @@ +import datetime + +from api.models import Device + +def get_or_create_station(station_info: dict): + station, created = Device.objects.get_or_create( + id = station_info['device'] + ) + if created: + station.device_name = station_info['device'] + station.firmware = station_info['firmware'] + station.last_update = datetime.datetime.now(datetime.timezone.utc) + station.api_key = station_info['apikey'] + station.save() + + return station From d04bd3fa54ee81872b2f69229cb7866bfb188f5a Mon Sep 17 00:00:00 2001 From: Nik Sauer Date: Fri, 20 Dec 2024 13:48:52 +0100 Subject: [PATCH 06/10] added CreateStationDataAPIView enpoint, altered fk to room to be null --- .../0005_alter_measurementnew_room.py | 20 ++++++ app/api/models.py | 2 +- app/api/urls_v1.py | 3 +- app/api/views.py | 67 ++++++++++++++++++- 4 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 app/api/migrations/0005_alter_measurementnew_room.py diff --git a/app/api/migrations/0005_alter_measurementnew_room.py b/app/api/migrations/0005_alter_measurementnew_room.py new file mode 100644 index 0000000..ae58f8b --- /dev/null +++ b/app/api/migrations/0005_alter_measurementnew_room.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.2 on 2024-12-20 12:48 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0004_alter_devicelogs_id_alter_measurementnew_id_and_more'), + ('campaign', '0008_alter_organizationinvitation_expiring_date'), + ] + + operations = [ + migrations.AlterField( + model_name='measurementnew', + name='room', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='campaign.room'), + ), + ] diff --git a/app/api/models.py b/app/api/models.py index cb107d3..4195067 100644 --- a/app/api/models.py +++ b/app/api/models.py @@ -86,7 +86,7 @@ class MeasurementNew(models.Model): time_measured = models.DateTimeField() sensor_model = models.IntegerField() device = models.ForeignKey(Device, on_delete=models.CASCADE) - room = models.ForeignKey(Room, on_delete=models.CASCADE) + room = models.ForeignKey(Room, on_delete=models.CASCADE, null=True) def __str__(self): return f'Measurement {self.id} from Device {self.device.id}' diff --git a/app/api/urls_v1.py b/app/api/urls_v1.py index 00b846f..58cde69 100644 --- a/app/api/urls_v1.py +++ b/app/api/urls_v1.py @@ -1,5 +1,5 @@ from django.urls import path -from .views import CreateStationStatusAPIView, AirQualityDataAddView, DeviceDetailView, DeviceDataAddView, DeviceStatusView, WorkshopDetailView, WorkshopAirQualityDataView +from .views import CreateStationStatusAPIView, AirQualityDataAddView, DeviceDetailView, DeviceDataAddView, DeviceStatusView, WorkshopDetailView, WorkshopAirQualityDataView, CreateStationDataAPIView urlpatterns = [ path('devices//', DeviceDetailView.as_view(), name='api-v1-device-detail'), @@ -9,4 +9,5 @@ path('workshops/data/add/', AirQualityDataAddView.as_view(), name='api-v1-air-quality-data-add'), path('workshops//', WorkshopDetailView.as_view(), name='api-v1-workshop-detail'), path('status/', CreateStationStatusAPIView.as_view(), name='station-status'), + path('data/', CreateStationDataAPIView.as_view(), name='station-data'), ] \ No newline at end of file diff --git a/app/api/views.py b/app/api/views.py index 7e93378..e17e1e3 100644 --- a/app/api/views.py +++ b/app/api/views.py @@ -1,3 +1,4 @@ +import datetime from django.db import IntegrityError, transaction from django.utils.dateparse import parse_datetime @@ -6,9 +7,10 @@ from rest_framework import status from rest_framework.generics import RetrieveAPIView from rest_framework.exceptions import ValidationError +from django.http import JsonResponse from main.util import get_or_create_station -from .models import AirQualityRecord, AirQualityDatapoint, MobilityMode, Measurement, DeviceLogs +from .models import AirQualityRecord, AirQualityDatapoint, MobilityMode, Measurement, DeviceLogs, Values, MeasurementNew from workshops.models import Participant, Workshop from devices.models import Device @@ -162,3 +164,66 @@ def post(self, request, *args, **kwargs): except Exception as e: return Response({"status": "error", "message": str(e)}, status=status.HTTP_400_BAD_REQUEST) + + +class CreateStationDataAPIView(APIView): + def post(self, request, *args, **kwargs): + # Parse the incoming JSON data + try: + station_data = request.data.get('station') + sensors_data = request.data.get('sensors') + + if not station_data or not sensors_data: + raise ValidationError("Both 'station' and 'sensors' are required.") + + # Use the get_or_create_station function to get or create the station + station = get_or_create_station(station_data) + + # Record the time when the request was received + time_received = datetime.datetime.now(datetime.timezone.utc) + + try: + with transaction.atomic(): + # Iterate through all sensors + for sensor_id, sensor_data in sensors_data.items(): + # Check if the measurement already exists in the database + existing_measurement = MeasurementNew.objects.filter( + device=station, + time_measured=station_data['time'], + sensor_model=sensor_data['type'] + ).first() + + if existing_measurement: + return JsonResponse( + {"status": "error", "detail": "Measurement already in Database"}, + status=422 + ) + + # If no existing measurement, create a new one + measurement = MeasurementNew( + sensor_model=sensor_data['type'], + device=station, + time_measured=station_data['time'], + time_received=time_received, + ) + measurement.save() + + # Add values (dimension, value) for the measurement + for dimension, value in sensor_data['data'].items(): + Values.objects.create( + dimension=dimension, + value=value, + measurement=measurement + ) + + # Update the station's last active time + station.last_update = station_data['time'] + station.save() + + return JsonResponse({"status": "success"}, status=201) + + except Exception as e: + return JsonResponse({"status": "error", "message": str(e)}, status=400) + + except Exception as e: + return JsonResponse({"status": "error", "message": str(e)}, status=400) From 9a828415c7c0e27298b59783c1bd995ee4658e9f Mon Sep 17 00:00:00 2001 From: Nik Sauer Date: Fri, 20 Dec 2024 14:12:11 +0100 Subject: [PATCH 07/10] removed * from allowed hosts --- app/main/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main/settings.py b/app/main/settings.py index d69880c..3ec97ad 100644 --- a/app/main/settings.py +++ b/app/main/settings.py @@ -37,7 +37,7 @@ "staging.arbeitsplatz.luftdaten.at", "localhost", "127.0.0.1", - "172.18.0.*" + "172.18.0.*", ] From e49d7c3c46597c5116b78ef1c3d7414333def5ce Mon Sep 17 00:00:00 2001 From: Nik Sauer Date: Thu, 2 Jan 2025 10:06:54 +0100 Subject: [PATCH 08/10] debug --- app/api/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/api/views.py b/app/api/views.py index e17e1e3..1e23587 100644 --- a/app/api/views.py +++ b/app/api/views.py @@ -137,6 +137,7 @@ def get(self, request, pk): class CreateStationStatusAPIView(APIView): def post(self, request, *args, **kwargs): + print(request) station_data = request.data.get('station') status_list = request.data.get('status_list', []) @@ -168,6 +169,7 @@ def post(self, request, *args, **kwargs): class CreateStationDataAPIView(APIView): def post(self, request, *args, **kwargs): + print(request) # Parse the incoming JSON data try: station_data = request.data.get('station') From 1f2ab99597880d4677fb2f6e4dd9277c3fb36fa6 Mon Sep 17 00:00:00 2001 From: Nik Sauer Date: Thu, 2 Jan 2025 10:32:10 +0100 Subject: [PATCH 09/10] change sucess code to 200, added apikey validation to stationdata --- app/api/views.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/api/views.py b/app/api/views.py index 1e23587..d8ccbdb 100644 --- a/app/api/views.py +++ b/app/api/views.py @@ -161,7 +161,7 @@ def post(self, request, *args, **kwargs): message=status_data.get('message', ''), # Default empty message if not provided ) - return Response({"status": "success"}, status=status.HTTP_201_CREATED) + return Response({"status": "success"}, status=200) except Exception as e: return Response({"status": "error", "message": str(e)}, status=status.HTTP_400_BAD_REQUEST) @@ -181,6 +181,9 @@ def post(self, request, *args, **kwargs): # Use the get_or_create_station function to get or create the station station = get_or_create_station(station_data) + if station.api_key != station_data.get('apikey'): + raise ValidationError("Wrong API Key") + # Record the time when the request was received time_received = datetime.datetime.now(datetime.timezone.utc) @@ -222,7 +225,7 @@ def post(self, request, *args, **kwargs): station.last_update = station_data['time'] station.save() - return JsonResponse({"status": "success"}, status=201) + return JsonResponse({"status": "success"}, status=200) except Exception as e: return JsonResponse({"status": "error", "message": str(e)}, status=400) From 9526fd81986c85a90a37bd5834492d492058809d Mon Sep 17 00:00:00 2001 From: Nik Sauer Date: Thu, 2 Jan 2025 11:10:44 +0100 Subject: [PATCH 10/10] changed lenght of firmware field to 255 --- .../migrations/0008_alter_device_firmware.py | 18 ++++++++++++++++++ app/devices/models.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 app/devices/migrations/0008_alter_device_firmware.py diff --git a/app/devices/migrations/0008_alter_device_firmware.py b/app/devices/migrations/0008_alter_device_firmware.py new file mode 100644 index 0000000..7623c1d --- /dev/null +++ b/app/devices/migrations/0008_alter_device_firmware.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-01-02 10:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('devices', '0007_remove_sensor_api_key_device_api_key'), + ] + + operations = [ + migrations.AlterField( + model_name='device', + name='firmware', + field=models.CharField(blank=True, max_length=255), + ), + ] diff --git a/app/devices/models.py b/app/devices/models.py index 8e93079..ba348ab 100644 --- a/app/devices/models.py +++ b/app/devices/models.py @@ -9,7 +9,7 @@ class Device(models.Model): id = models.CharField(max_length=255, primary_key=True) device_name = models.CharField(max_length=255, blank=True, null=True) model = models.CharField(max_length=255, blank=True, null=True) - firmware = models.CharField(max_length=12, blank=True) + firmware = models.CharField(max_length=255, blank=True) btmac_address = models.CharField(max_length=12, null=True, blank=True) last_update = models.DateTimeField(null=True, blank=True) notes = models.TextField(null=True, blank=True)