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.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..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 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'), @@ -8,4 +8,6 @@ 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'), + 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 7395a4e..d8ccbdb 100644 --- a/app/api/views.py +++ b/app/api/views.py @@ -1,12 +1,16 @@ -from django.db import IntegrityError +import datetime +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 django.http import JsonResponse -from .models import AirQualityRecord, AirQualityDatapoint, MobilityMode, Measurement +from main.util import get_or_create_station +from .models import AirQualityRecord, AirQualityDatapoint, MobilityMode, Measurement, DeviceLogs, Values, MeasurementNew from workshops.models import Participant, Workshop from devices.models import Device @@ -128,4 +132,103 @@ 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): + print(request) + 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 = get_or_create_station(station_info=station_data) + + if station.api_key != station_data.get('apikey'): + raise ValidationError("Wrong API Key") + + 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=200) + + 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): + print(request) + # 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) + + 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) + + 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=200) + + 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) 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 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/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/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 8b20ed9..ba348ab 100644 --- a/app/devices/models.py +++ b/app/devices/models.py @@ -9,10 +9,11 @@ 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) + api_key = models.CharField(max_length=64, null=True) def __str__(self): return self.id or "Undefined Device" # Added fallback for undefined IDs 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.*", ] diff --git a/app/main/util.py b/app/main/util.py new file mode 100644 index 0000000..bc4f7aa --- /dev/null +++ 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