diff --git a/.gitignore b/.gitignore index 44d348d..085bb12 100644 --- a/.gitignore +++ b/.gitignore @@ -400,3 +400,7 @@ celerybeat.pid logs/ /static + + +#folder in project directory to store temporary files or backup files locally +temp/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 0ccc619..f9ffdd2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.6.3 +FROM python:3.9.7 ENV PYTHONUNBUFFERED 1 @@ -17,7 +17,7 @@ ADD . /src/ RUN pip install -q -U pip setuptools # Install feinstaub from opendata-stuttgart -RUN pip install -q git+https://github.com/opendata-stuttgart/feinstaub-api +# RUN pip install -q git+https://github.com/opendata-stuttgart/feinstaub-api # Install sensors.AFRICA-api and its dependencies RUN pip install -q -U . diff --git a/README.md b/README.md index 1f70360..d8262c7 100644 --- a/README.md +++ b/README.md @@ -12,24 +12,46 @@ Gitignore is standardized for this project using [gitignore.io](https://www.giti To get the project up and running: - Clone this repo - +- +### Prerequisites + + - Have [PostgreSQL](https://www.postgresql.org/download/) installed + - Have **psql** installed - a command line tool to interact with PostgreSQL. To install, follow instructions as shown [here](https://www.timescale.com/blog/how-to-install-psql-on-mac-ubuntu-debian-windows/). + - Have a Python version 3.9.7 installed in your system. If you are using a different Python version you could use a tool like [Pyenv](https://github.com/pyenv/pyenv) to manage different versions ### Virtual environment - -- Use virtualenv to create your virtual environment; `virtualenv venv` + +- Use virtualenv to create your virtual environment; `python -m venv venv` or `virtualenv venv` - Activate the virtual environment; `source venv/bin/activate` -- Install feinstaub; `pip install git+https://github.com/opendata-stuttgart/feinstaub-api` -- Install the requirements; `pip install .` -- Create a sensorsafrica database with the following sql script: +- ~~Install feinstaub; `pip install git+https://github.com/opendata-stuttgart/feinstaub-api`~~ issues detected. Feinstaub sensors app manually include in project directory +- ~~Install the requirements; `pip install .`~~ Dependency hell with older conflicting module versions. +- Run the below commands in the virtual environment to install depencencies. Note : latest versions will be installed +```bash + +pip install —upgrade pip +pip install —upgrade setuptools + +pip install django django-cors-headers django-filter djangorestframework coreapi celery celery_slack python-dateutil timeago psycopg2-binary dj_database_url sentry_sdk django_extensions whitenoise + +``` + +### Database setup +- Create a sensorsafrica database open your terminal and hit `psql postgres`, then run following sql script: ```sql CREATE DATABASE sensorsafrica; CREATE USER sensorsafrica WITH ENCRYPTED PASSWORD 'sensorsafrica'; GRANT ALL PRIVILEGES ON DATABASE sensorsafrica TO sensorsafrica; ``` +### Running the app + +Still in your virtual enviroment, run the following: - Migrate the database; `python manage.py migrate` - Run the server; `python manage.py runserver` + +--- + ### Docker Using docker compose: diff --git a/sensorsafrica/openstuttgart/__init__.py b/feinstaub/__init__.py similarity index 100% rename from sensorsafrica/openstuttgart/__init__.py rename to feinstaub/__init__.py diff --git a/sensorsafrica/openstuttgart/feinstaub/__init__.py b/feinstaub/main/__init__.py similarity index 100% rename from sensorsafrica/openstuttgart/feinstaub/__init__.py rename to feinstaub/main/__init__.py diff --git a/feinstaub/main/admin.py b/feinstaub/main/admin.py new file mode 100644 index 0000000..236a19c --- /dev/null +++ b/feinstaub/main/admin.py @@ -0,0 +1,18 @@ +from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.models import User +from django.contrib import admin + +from .models import UserProfile + + +class UserProfileInline(admin.StackedInline): + model = UserProfile + can_delete = False + + +class UserAdmin(UserAdmin): + inlines = (UserProfileInline, ) + + +admin.site.unregister(User) +admin.site.register(User, UserAdmin) diff --git a/feinstaub/main/migrations/0001_initial.py b/feinstaub/main/migrations/0001_initial.py new file mode 100644 index 0000000..c1bfa2e --- /dev/null +++ b/feinstaub/main/migrations/0001_initial.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import django_extensions.db.fields +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='UserProfile', + fields=[ + ('id', models.AutoField(auto_created=True, serialize=False, verbose_name='ID', primary_key=True)), + ('created', django_extensions.db.fields.CreationDateTimeField(verbose_name='created', auto_now_add=True)), + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), + ('notification_type', models.CharField(max_length=100, choices=[('none', 'no notification'), ('email', 'email'), ('pushover', 'pushover'), ('notifymyandroid', 'notifymyandroid')])), + ('pushover_clientkey', models.CharField(max_length=100, blank=True, default='')), + ('notifymyandroid_apikey', models.CharField(max_length=100, blank=True, default='')), + ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, related_name='profile',on_delete=models.CASCADE)), + ], + options={ + 'ordering': ('-modified', '-created'), + 'get_latest_by': 'modified', + 'abstract': False, + }, + ), + ] diff --git a/feinstaub/main/migrations/0002_alter_userprofile_options.py b/feinstaub/main/migrations/0002_alter_userprofile_options.py new file mode 100644 index 0000000..fc5fdb1 --- /dev/null +++ b/feinstaub/main/migrations/0002_alter_userprofile_options.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.1 on 2023-05-28 11:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("main", "0001_initial"), + ] + + operations = [ + migrations.AlterModelOptions( + name="userprofile", options={"get_latest_by": "modified"}, + ), + ] diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/__init__.py b/feinstaub/main/migrations/__init__.py similarity index 100% rename from sensorsafrica/openstuttgart/feinstaub/sensors/__init__.py rename to feinstaub/main/migrations/__init__.py diff --git a/feinstaub/main/models.py b/feinstaub/main/models.py new file mode 100644 index 0000000..25f5de7 --- /dev/null +++ b/feinstaub/main/models.py @@ -0,0 +1,18 @@ +from django.contrib.auth.models import User +from django.db import models +from django_extensions.db.models import TimeStampedModel + + +class UserProfile(TimeStampedModel): + NOTIFICATION_TYPE_CHOICES = (('none', 'no notification'), + ('email', 'email'), + ('pushover', 'pushover'), + ('notifymyandroid', 'notifymyandroid'),) + + user = models.OneToOneField(User, on_delete=models.CASCADE,related_name='profile') + notification_type = models.CharField(max_length=100, choices=NOTIFICATION_TYPE_CHOICES) + pushover_clientkey = models.CharField(max_length=100, default='', blank=True) + notifymyandroid_apikey = models.CharField(max_length=100, default='', blank=True) + + def __str__(self): + return str(self.user) diff --git a/feinstaub/main/serializers.py b/feinstaub/main/serializers.py new file mode 100644 index 0000000..aff5d2a --- /dev/null +++ b/feinstaub/main/serializers.py @@ -0,0 +1,19 @@ +from django.contrib.auth.models import User +from rest_framework import serializers + +from .models import UserProfile + + +class ProfileSerializer(serializers.ModelSerializer): + + class Meta: + model = UserProfile + fields = ("notification_type", "pushover_clientkey", "notifymyandroid_apikey") + + +class UserSerializer(serializers.ModelSerializer): + profile = ProfileSerializer() + + class Meta: + model = User + fields = ('id', 'username', 'email', 'profile') diff --git a/feinstaub/main/views.py b/feinstaub/main/views.py new file mode 100644 index 0000000..cf48b5f --- /dev/null +++ b/feinstaub/main/views.py @@ -0,0 +1,24 @@ +from django.contrib.auth.models import User +from rest_framework import mixins, viewsets, filters + +from .serializers import UserSerializer + + +class UsersView(mixins.ListModelMixin, + mixins.RetrieveModelMixin, + viewsets.GenericViewSet): + """ Get more information about users + """ + + serializer_class = UserSerializer + filter_backends = (filters.OrderingFilter, ) + ordering = ('id', ) + queryset = User.objects.all() + + def get_queryset(self): + if not self.request.user.is_authenticated(): + return User.objects.none() + + if self.request.user.groups.filter(name="show_me_everything").exists(): + return User.objects.all() + return User.objects.filter(pk=self.request.user.pk) diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/__init__.py b/feinstaub/sensors/__init__.py similarity index 100% rename from sensorsafrica/openstuttgart/feinstaub/sensors/migrations/__init__.py rename to feinstaub/sensors/__init__.py diff --git a/feinstaub/sensors/admin.py b/feinstaub/sensors/admin.py new file mode 100644 index 0000000..686f69c --- /dev/null +++ b/feinstaub/sensors/admin.py @@ -0,0 +1,61 @@ +# coding=utf-8 +from django.contrib import admin + +from .models import ( + Node, + Sensor, + SensorData, + SensorDataValue, + SensorLocation, + SensorType, +) + + +@admin.register(Node) +class NodeAdmin(admin.ModelAdmin): + search_fields = ['uid', 'description'] + list_display = ['uid', 'owner', 'location', + 'description', 'created', 'modified'] + list_filter = ['owner', 'location'] + + +@admin.register(Sensor) +class SensorAdmin(admin.ModelAdmin): + search_fields = ['node__uid', 'description'] + list_display = ['node', 'pin', 'sensor_type', + 'description', 'created', 'modified'] + list_filter = ['node__owner', 'sensor_type'] + + +@admin.register(SensorData) +class SensorDataAdmin(admin.ModelAdmin): + search_fields = ['sensor__uid', ] + list_display = ['sensor', 'sampling_rate', 'timestamp', + 'location', 'created', 'modified'] + list_filter = ['sensor', 'location', 'sensor__sensor_type'] + show_full_result_count = False + + +@admin.register(SensorDataValue) +class SensorDataValueAdmin(admin.ModelAdmin): + list_display = ['sensordata', 'value_type', 'value', + 'created', 'modified'] + list_filter = ['value_type', 'sensordata__sensor', + 'sensordata__sensor__sensor_type'] + readonly_fields = ['sensordata'] + show_full_result_count = False + + +@admin.register(SensorLocation) +class SensorLocationAdmin(admin.ModelAdmin): + search_fields = ['location', ] + list_display = ['location', 'latitude', 'longitude', 'indoor', 'owner', 'description', + 'timestamp', 'created', 'modified'] + list_filter = ['indoor', 'owner'] + + +@admin.register(SensorType) +class SensorTypeAdmin(admin.ModelAdmin): + search_fields = ['uid', 'name', 'manufacturer', 'description'] + list_display = ['uid', 'name', 'manufacturer', + 'description', 'created', 'modified'] diff --git a/feinstaub/sensors/authentication.py b/feinstaub/sensors/authentication.py new file mode 100644 index 0000000..2207e20 --- /dev/null +++ b/feinstaub/sensors/authentication.py @@ -0,0 +1,48 @@ +from rest_framework import authentication +from rest_framework import permissions +from rest_framework import exceptions + +from .models import Node, Sensor, SensorData, SensorDataValue + + +class NodeUidAuthentication(authentication.BaseAuthentication): + def authenticate(self, request): + node_uid = request.META.get('HTTP_X_SENSOR') or request.META.get('HTTP_SENSOR') or request.META.get('HTTP_NODE') + if not node_uid: + return None + + node_pin = request.META.get('HTTP_X_PIN') or request.META.get('HTTP_PIN', '-') + + try: + node = Node.objects.get(uid=node_uid) + except Node.DoesNotExist: + raise exceptions.AuthenticationFailed('Node not found in database.') + + return (node, node_pin) + + +class OwnerPermission(permissions.BasePermission): + """Checks if authenticated user is owner of the node""" + + def has_object_permission(self, request, view, obj): + if isinstance(obj, SensorDataValue): + owner_pk = SensorDataValue.objects \ + .filter(pk=obj.pk) \ + .values_list('sensordata__sensor__node__owner_id', flat=True) \ + .first() + elif isinstance(obj, SensorData): + owner_pk = SensorData.objects \ + .filter(pk=obj.pk) \ + .values_list('sensor__node__owner_id', flat=True) \ + .first() + elif isinstance(obj, Sensor): + owner_pk = Sensor.objects \ + .filter(pk=obj.pk) \ + .values_list('node__owner_id', flat=True) \ + .first() + elif isinstance(obj, Node): + owner_pk = obj.owner_id + else: + return False + + return request.user.pk == owner_pk diff --git a/feinstaub/sensors/forms.py b/feinstaub/sensors/forms.py new file mode 100644 index 0000000..7ce2e71 --- /dev/null +++ b/feinstaub/sensors/forms.py @@ -0,0 +1,46 @@ +from django import forms + +from .models import SensorType + + +class AddSensordeviceForm(forms.Form): + # more fields, see https://docs.djangoproject.com/en/1.10/ref/forms/fields/ + + # get user information + name_pate = forms.CharField(label='Your name', initial="") + email_pate = forms.EmailField(label='Your email', required=True) + # dbuser = forms.CharField(label='DB user', required=True) # get from login data + + # Location information + # use exiting location possible? + # maybe with selection from class SensorLocation(TimeStampedModel): location + # and + # use_location_fk = forms.BooleanField(label='use existing location', required=True, initial=False) + # location: manual input + location_location = forms.CharField(label='Location name (Address)', initial="") + location_description = forms.CharField(label='Location description', initial="", widget=forms.Textarea) + location_latitude = forms.DecimalField(label='Latitude (Breite, ~48)', min_value=-90, max_value=90, decimal_places=10) + location_longitude = forms.DecimalField(label='Longitude (Länge, 9)', min_value=-180, max_value=180, decimal_places=10) + + # device info + device_initials = forms.CharField(label='Device initials (label to write on device)') + device_uid = forms.CharField(label='Device UID (esp8266-)', initial="esp8266-") + + # Sensor info + # multiple devices possible, have 2 as default + # insert into model class Sensor(TimeStampedModel): + + # TODO: queryset + # class SensorType(TimeStampedModel): default: SDS011 14 + sensor1_type = forms.ModelChoiceField(queryset=SensorType.objects.all(), to_field_name="name", initial="SDS011") + sensor1_pin = forms.DecimalField(label='PIN', min_value=0, max_value=8, decimal_places=0, initial=1) + sensor1_description = forms.CharField(label='description for sensor 1', widget=forms.Textarea) + sensor1_public = forms.BooleanField(label='public', required=True, initial=True) + + # TODO: queryset + # class SensorType(TimeStampedModel): default: DHT22 9 + sensor2_type = forms.ModelChoiceField(queryset=SensorType.objects.all(), to_field_name="name", initial="DHT22", empty_label='--- No sensor ---', required=False) + sensor2_pin = forms.DecimalField(label='PIN', min_value=0, max_value=8, decimal_places=0, initial=7) + # sensor description should contain deviceinitials+"_"+sensor1_type + sensor2_description = forms.CharField(label='description for sensor 2') + sensor2_public = forms.BooleanField(label='public', required=True, initial=True) diff --git a/feinstaub/sensors/management/__init__.py b/feinstaub/sensors/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/feinstaub/sensors/management/commands/__init__.py b/feinstaub/sensors/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/feinstaub/sensors/management/commands/cleanup.py b/feinstaub/sensors/management/commands/cleanup.py new file mode 100644 index 0000000..3b244ed --- /dev/null +++ b/feinstaub/sensors/management/commands/cleanup.py @@ -0,0 +1,19 @@ +# coding=utf-8 +from django.core.management import BaseCommand +import datetime + + +class Command(BaseCommand): + + help = "Cleanup sensordata. This management command may change over time." + + def handle(self, *args, **options): + from sensor.models import SensorData + from django.db.models import Count + + # delete all SensorData without any SensorDataValues, older than today (maybe some are written just now). + SensorData.objects.annotate(Count('sensordatavalues')).filter(sensordatavalues__count=0).filter(created__lt=datetime.date.today()).delete() + + # find all ppd42ns with wrong values and delete them. fixing is way to complicated + SensorData.objects.filter(sensor_id__in=[34, 39, 60]).filter(sensordatavalues__value_type="temperature").delete() + SensorData.objects.filter(sensor_id__in=[34, 39, 60]).filter(sensordatavalues__value_type="humidity").delete() diff --git a/feinstaub/sensors/management/commands/export_as_csv.py b/feinstaub/sensors/management/commands/export_as_csv.py new file mode 100644 index 0000000..346296e --- /dev/null +++ b/feinstaub/sensors/management/commands/export_as_csv.py @@ -0,0 +1,164 @@ +# coding=utf-8 +import os +import datetime +from itertools import product +import boto3 + +from django.core.management import BaseCommand + + +def str2date(str, default): + return datetime.datetime.strptime(str, '%Y-%m-%d').date() if str else default + + +class Command(BaseCommand): + + help = "Dump all Sensordata to csv files" + + def add_arguments(self, parser): + parser.add_argument('--start_date') + parser.add_argument('--end_date') + parser.add_argument('--type') + parser.add_argument('--no_excludes', action="store_false") + parser.add_argument('--upload_s3') + + def handle(self, *args, **options): + from sensors.models import Sensor, SensorData + + # default yesterday + yesterday = datetime.date.today() - datetime.timedelta(days=1) + start_date = str2date(options.get('start_date'), yesterday) + end_date = str2date(options.get('end_date'), yesterday) + sensor_type = (options.get('type') or 'ppd42ns').lower() + + if start_date > end_date: + print("end_date is before start_date") + return + + folder = "/opt/code/archive" + + for dt, sensor in product(self._dates(start_date, end_date), Sensor.objects.all()): + # first only for ppd42ns. + # because we need a list of fields for all other sensors + # -> SENSOR_TYPE_CHOICES needs to become more sophisticated + if not sensor.sensor_type.name.lower() == sensor_type: + continue + + # location 11 is the dummy location. remove the datasets. + # remove all indoor locations + qs = SensorData.objects \ + .filter(sensor=sensor) \ + .filter(timestamp__date=dt) \ + .order_by("timestamp") + if options.get('no_excludes'): + qs = qs.exclude(location_id=11) \ + .exclude(location__indoor=True) + if not qs.exists(): + continue + + fn = '{date}_{stype}_sensor_{sid}.csv'.format( + date=str(dt), + stype=sensor.sensor_type.name.lower(), + sid=sensor.id, + ) + print(fn) + os.makedirs(os.path.join(folder, str(dt)), exist_ok=True) + + # if file exists; overwrite. always + self._write_file( + filepath=os.path.join(folder, str(dt), fn), + qs=qs, + sensor=sensor, + ) + + # Upload to s3 + if options.get('upload_s3'): + # if file exists on s3; overwrite. always + self._upload_csv( + tmp_path=os.path.join(folder, str(dt), fn), + dest_filename=os.path.join(str(dt), fn), + content_type="csv" + ) + + @staticmethod + def _write_file(filepath, qs, sensor): + sensor_type = sensor.sensor_type.name.lower() + if sensor_type == 'ppd42ns' or sensor_type == 'sds011': + key_list = ['P1', 'durP1', 'ratioP1', 'P2', 'durP2', 'ratioP2'] + elif sensor_type == 'pms1003' or sensor_type == 'pms3003' or sensor_type == 'pms5003' or sensor_type == 'pms6003' or sensor_type == 'pms7003': + key_list = ['P1', 'P2', 'P0'] + elif sensor_type in ['ds18b20']: + key_list = ['temperature'] + elif sensor_type in ['dht11', 'dht22', 'htu21d', 'sht10', 'sht11', 'sht15']: + key_list = ['temperature', 'humidity'] + elif sensor_type == "bmp180" or sensor_type == 'bpm280': + key_list = ['pressure', 'altitude', 'pressure_sealevel', 'temperature'] + elif sensor_type == "bme280": + key_list = ['pressure', 'altitude', 'pressure_sealevel', 'temperature','humidity'] + elif sensor_type == "photoresistor": + key_list = ['brightness'] + else: + key_list = [] + + with open(filepath, "w") as fp: + fp.write("sensor_id;sensor_type;location;lat;lon;timestamp;") + fp.write(';'.join(key_list)) + fp.write("\n") + for sd in qs: + sensordata = { + data['value_type']: data['value'] + for data in sd.sensordatavalues.values('value_type', 'value') + } + if not sensordata: + continue + if sensor_type == 'ppd42ns' and 'P1' not in sensordata: + continue + + longitude = '' + if sd.location.longitude: + longitude = "{:.3f}".format(sd.location.longitude) + latitude = '' + if sd.location.latitude: + latitude = "{:.3f}".format(sd.location.latitude) + s = ';'.join([str(sensor.id), + sensor.sensor_type.name, + str(sd.location.id), + latitude, + longitude, + sd.timestamp.isoformat()]) + + fp.write(s) + fp.write(';') + fp.write(';'.join([sensordata.get(i, '') for i in key_list])) + fp.write("\n") + + @staticmethod + def _dates(start, end): + current = start + while current <= end: + yield current + current += datetime.timedelta(days=1) + + @staticmethod + def _upload_csv(tmp_path, dest_filename, content_type): + bucket_name = os.getenv('AWS_BUCKET_NAME') + access_key = os.getenv('AWS_ACCESS_KEY') + secret_access_key = os.getenv('AWS_SECRET_ACCESS_KEY') + url_prefix = os.getenv('AWS_URL_PREFIX') + region = os.getenv('AWS_REGION') + + dest_path = os.path.join(url_prefix, dest_filename) + url = 'http://s3-%s.amazonaws.com/%s/%s' % (region, bucket_name, dest_path) + + session = boto3.Session(aws_access_key_id=access_key, aws_secret_access_key=secret_access_key, + region_name=region) + s3 = session.resource('s3') + + bucket = s3.Bucket(bucket_name) + try: + bucket.upload_file(tmp_path, dest_path, ExtraArgs={'ContentType': content_type, + 'ContentDisposition': 'attachment', + 'ACL': "public-read"}) + return url + except IOError as e: + return None diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0001_initial.py b/feinstaub/sensors/migrations/0001_initial.py similarity index 96% rename from sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0001_initial.py rename to feinstaub/sensors/migrations/0001_initial.py index c0f7a0b..48bfc5c 100644 --- a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0001_initial.py +++ b/feinstaub/sensors/migrations/0001_initial.py @@ -22,7 +22,7 @@ class Migration(migrations.Migration): ('modified', django_extensions.db.fields.ModificationDateTimeField(blank=True, default=django.utils.timezone.now, editable=False, verbose_name='modified')), ('uid', models.SlugField(unique=True)), ('description', models.CharField(max_length=10000)), - ('owner', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ('owner', models.ForeignKey(to=settings.AUTH_USER_MODEL,on_delete=models.CASCADE)), ], options={ 'get_latest_by': 'modified', @@ -39,7 +39,7 @@ class Migration(migrations.Migration): ('modified', django_extensions.db.fields.ModificationDateTimeField(blank=True, default=django.utils.timezone.now, editable=False, verbose_name='modified')), ('value1', models.IntegerField()), ('value2', models.IntegerField(blank=True, null=True)), - ('sensor', models.ForeignKey(to='sensors.Sensor')), + ('sensor', models.ForeignKey(to='sensors.Sensor',on_delete=models.CASCADE)), ], options={ 'get_latest_by': 'modified', @@ -55,7 +55,7 @@ class Migration(migrations.Migration): ('created', django_extensions.db.fields.CreationDateTimeField(blank=True, default=django.utils.timezone.now, editable=False, verbose_name='created')), ('modified', django_extensions.db.fields.ModificationDateTimeField(blank=True, default=django.utils.timezone.now, editable=False, verbose_name='modified')), ('location', models.TextField(blank=True, null=True)), - ('sensor', models.ForeignKey(to='sensors.Sensor')), + ('sensor', models.ForeignKey(to='sensors.Sensor',on_delete=models.CASCADE)), ], options={ 'get_latest_by': 'modified', @@ -85,7 +85,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='sensor', name='sensor_type', - field=models.ForeignKey(to='sensors.SensorType'), + field=models.ForeignKey(to='sensors.SensorType',on_delete=models.CASCADE), preserve_default=True, ), ] diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0002_auto_20150330_1800.py b/feinstaub/sensors/migrations/0002_auto_20150330_1800.py similarity index 100% rename from sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0002_auto_20150330_1800.py rename to feinstaub/sensors/migrations/0002_auto_20150330_1800.py diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0003_auto_20150330_1805.py b/feinstaub/sensors/migrations/0003_auto_20150330_1805.py similarity index 100% rename from sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0003_auto_20150330_1805.py rename to feinstaub/sensors/migrations/0003_auto_20150330_1805.py diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0004_auto_20150331_1907.py b/feinstaub/sensors/migrations/0004_auto_20150331_1907.py similarity index 95% rename from sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0004_auto_20150331_1907.py rename to feinstaub/sensors/migrations/0004_auto_20150331_1907.py index 3de14f2..191de04 100644 --- a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0004_auto_20150331_1907.py +++ b/feinstaub/sensors/migrations/0004_auto_20150331_1907.py @@ -23,7 +23,7 @@ class Migration(migrations.Migration): ('modified', django_extensions.db.fields.ModificationDateTimeField(editable=False, default=django.utils.timezone.now, blank=True, verbose_name='modified')), ('value', models.TextField()), ('value_type', models.CharField(choices=[('P1', '1µm particles'), ('P2', '2.5µm particles'), ('temperature', 'Temperature'), ('humidity', 'Humidity'), ('brightness', 'Brightness')], max_length=100)), - ('sensordata', models.ForeignKey(to='sensors.SensorData')), + ('sensordata', models.ForeignKey(to='sensors.SensorData',on_delete=models.CASCADE)), ], options={ 'abstract': False, @@ -47,13 +47,13 @@ class Migration(migrations.Migration): migrations.AddField( model_name='sensor', name='location', - field=models.ForeignKey(default=False, to='sensors.SensorLocation'), + field=models.ForeignKey(default=False, to='sensors.SensorLocation',on_delete=models.CASCADE), preserve_default=False, ), migrations.AddField( model_name='sensordata', name='location', - field=models.ForeignKey(default=1, blank=True, to='sensors.SensorLocation'), + field=models.ForeignKey(default=1, blank=True, to='sensors.SensorLocation',on_delete=models.CASCADE), preserve_default=False, ), migrations.AddField( @@ -71,7 +71,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='sensorlocation', name='owner', - field=models.ForeignKey(blank=True, help_text='If not set, location is public.', null=True, to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(blank=True, help_text='If not set, location is public.', null=True, to=settings.AUTH_USER_MODEL,on_delete=models.CASCADE), preserve_default=True, ), migrations.AlterField( diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0005_auto_20150403_2041.py b/feinstaub/sensors/migrations/0005_auto_20150403_2041.py similarity index 100% rename from sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0005_auto_20150403_2041.py rename to feinstaub/sensors/migrations/0005_auto_20150403_2041.py diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0006_auto_20150404_2050.py b/feinstaub/sensors/migrations/0006_auto_20150404_2050.py similarity index 100% rename from sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0006_auto_20150404_2050.py rename to feinstaub/sensors/migrations/0006_auto_20150404_2050.py diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0007_auto_20150405_2151.py b/feinstaub/sensors/migrations/0007_auto_20150405_2151.py similarity index 88% rename from sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0007_auto_20150405_2151.py rename to feinstaub/sensors/migrations/0007_auto_20150405_2151.py index 3dbe751..89cb9ea 100644 --- a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0007_auto_20150405_2151.py +++ b/feinstaub/sensors/migrations/0007_auto_20150405_2151.py @@ -14,6 +14,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='sensordatavalue', name='sensordata', - field=models.ForeignKey(to='sensors.SensorData', related_name='sensordatavalues'), + field=models.ForeignKey(to='sensors.SensorData', related_name='sensordatavalues',on_delete=models.CASCADE), ), ] diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0008_auto_20150503_1554.py b/feinstaub/sensors/migrations/0008_auto_20150503_1554.py similarity index 100% rename from sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0008_auto_20150503_1554.py rename to feinstaub/sensors/migrations/0008_auto_20150503_1554.py diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0009_auto_20150503_1556.py b/feinstaub/sensors/migrations/0009_auto_20150503_1556.py similarity index 100% rename from sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0009_auto_20150503_1556.py rename to feinstaub/sensors/migrations/0009_auto_20150503_1556.py diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0010_auto_20150620_1708.py b/feinstaub/sensors/migrations/0010_auto_20150620_1708.py similarity index 100% rename from sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0010_auto_20150620_1708.py rename to feinstaub/sensors/migrations/0010_auto_20150620_1708.py diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0011_auto_20150807_1927.py b/feinstaub/sensors/migrations/0011_auto_20150807_1927.py similarity index 87% rename from sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0011_auto_20150807_1927.py rename to feinstaub/sensors/migrations/0011_auto_20150807_1927.py index 75965bd..683ee5f 100644 --- a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0011_auto_20150807_1927.py +++ b/feinstaub/sensors/migrations/0011_auto_20150807_1927.py @@ -12,14 +12,12 @@ def migrate_sensor(apps, schema_editor): # if we directly import it, it'll be the wrong version Sensor = apps.get_model("sensors", "Sensor") Node = apps.get_model("sensors", "Node") - db_alias = schema_editor.connection.alias - if db_alias != "default": - return - for sensor in Sensor.objects.using(db_alias).all(): - node = Node.objects.using(db_alias).create(uid=sensor.uid, - description=sensor.description, - owner=sensor.owner, - location=sensor.location) + for sensor in Sensor.objects.all(): + print("sensor: {}".format(sensor.id)) + node = Node.objects.create(uid=sensor.uid, + description=sensor.description, + owner=sensor.owner, + location=sensor.location) sensor.node = node sensor.save() @@ -48,17 +46,17 @@ class Migration(migrations.Migration): migrations.AddField( model_name='node', name='location', - field=models.ForeignKey(to='sensors.SensorLocation'), + field=models.ForeignKey(to='sensors.SensorLocation',on_delete=models.CASCADE), ), migrations.AddField( model_name='node', name='owner', - field=models.ForeignKey(to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(to=settings.AUTH_USER_MODEL,on_delete=models.CASCADE), ), migrations.AddField( model_name='sensor', name='node', - field=models.ForeignKey(to='sensors.Node', blank=True, null=True), + field=models.ForeignKey(to='sensors.Node', blank=True, null=True,on_delete=models.CASCADE), preserve_default=False, ), diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0012_auto_20150807_1943.py b/feinstaub/sensors/migrations/0012_auto_20150807_1943.py similarity index 94% rename from sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0012_auto_20150807_1943.py rename to feinstaub/sensors/migrations/0012_auto_20150807_1943.py index cb6181c..cb8960b 100644 --- a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0012_auto_20150807_1943.py +++ b/feinstaub/sensors/migrations/0012_auto_20150807_1943.py @@ -14,7 +14,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='sensor', name='node', - field=models.ForeignKey(default=1, to='sensors.Node'), + field=models.ForeignKey(default=1, to='sensors.Node',on_delete=models.CASCADE), preserve_default=False, ), ] diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0013_auto_20151025_1615.py b/feinstaub/sensors/migrations/0013_auto_20151025_1615.py similarity index 97% rename from sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0013_auto_20151025_1615.py rename to feinstaub/sensors/migrations/0013_auto_20151025_1615.py index f53be2f..d12359a 100644 --- a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0013_auto_20151025_1615.py +++ b/feinstaub/sensors/migrations/0013_auto_20151025_1615.py @@ -45,7 +45,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='sensor', name='node', - field=models.ForeignKey(related_name='sensors', to='sensors.Node'), + field=models.ForeignKey(related_name='sensors', to='sensors.Node',on_delete=models.CASCADE), ), migrations.AlterField( model_name='sensordata', @@ -60,7 +60,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='sensordata', name='sensor', - field=models.ForeignKey(related_name='sensordatas', to='sensors.Sensor'), + field=models.ForeignKey(related_name='sensordatas', to='sensors.Sensor',on_delete=models.CASCADE), ), migrations.AlterField( model_name='sensordatavalue', diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0014_sensor_public.py b/feinstaub/sensors/migrations/0014_sensor_public.py similarity index 100% rename from sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0014_sensor_public.py rename to feinstaub/sensors/migrations/0014_sensor_public.py diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0015_sensordata_software_version.py b/feinstaub/sensors/migrations/0015_sensordata_software_version.py similarity index 100% rename from sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0015_sensordata_software_version.py rename to feinstaub/sensors/migrations/0015_sensordata_software_version.py diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0016_auto_20160209_2030.py b/feinstaub/sensors/migrations/0016_auto_20160209_2030.py similarity index 100% rename from sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0016_auto_20160209_2030.py rename to feinstaub/sensors/migrations/0016_auto_20160209_2030.py diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0017_auto_20160416_1803.py b/feinstaub/sensors/migrations/0017_auto_20160416_1803.py similarity index 100% rename from sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0017_auto_20160416_1803.py rename to feinstaub/sensors/migrations/0017_auto_20160416_1803.py diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0018_auto_20170218_2329.py b/feinstaub/sensors/migrations/0018_auto_20170218_2329.py similarity index 100% rename from sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0018_auto_20170218_2329.py rename to feinstaub/sensors/migrations/0018_auto_20170218_2329.py diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0019_auto_20190125_0521.py b/feinstaub/sensors/migrations/0019_auto_20190125_0521.py similarity index 100% rename from sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0019_auto_20190125_0521.py rename to feinstaub/sensors/migrations/0019_auto_20190125_0521.py diff --git a/feinstaub/sensors/migrations/0020_alter_sensordata_options_node_exact_location_and_more.py b/feinstaub/sensors/migrations/0020_alter_sensordata_options_node_exact_location_and_more.py new file mode 100644 index 0000000..9416787 --- /dev/null +++ b/feinstaub/sensors/migrations/0020_alter_sensordata_options_node_exact_location_and_more.py @@ -0,0 +1,119 @@ +# Generated by Django 4.2.1 on 2023-05-28 11:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("sensors", "0019_auto_20190125_0521"), + ] + + operations = [ + migrations.AlterModelOptions( + name="sensordata", options={"get_latest_by": "modified"}, + ), + migrations.AddField( + model_name="node", + name="exact_location", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="node", + name="inactive", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="node", name="indoor", field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="sensorlocation", + name="altitude", + field=models.DecimalField( + blank=True, decimal_places=8, max_digits=14, null=True + ), + ), + migrations.AlterField( + model_name="sensordatavalue", name="value", field=models.TextField(), + ), + migrations.AlterField( + model_name="sensordatavalue", + name="value_type", + field=models.CharField( + choices=[ + ("P0", "1µm particles"), + ("P1", "10µm particles"), + ("P2", "2.5µm particles"), + ("durP1", "duration 1µm"), + ("durP2", "duration 2.5µm"), + ("ratioP1", "ratio 1µm in percent"), + ("ratioP2", "ratio 2.5µm in percent"), + ("samples", "samples"), + ("interval", "measurement interval"), + ("min_micro", "min_micro"), + ("max_micro", "max_micro"), + ("N05", "count 0.5µm particles"), + ("N1", "count 1µm particles"), + ("N25", "count 2.5µm particles"), + ("N4", "count 4µm particles"), + ("N10", "count 1µm particles"), + ("TS", "typical particle size"), + ("temperature", "Temperature"), + ("humidity", "Humidity"), + ("pressure", "Pa"), + ("altitude", "meter"), + ("pressure_sealevel", "Pa (sealevel)"), + ("brightness", "Brightness"), + ("dust_density", "Dust density in mg/m3"), + ("vo_raw", "Dust voltage raw"), + ("voltage", "Dust voltage calculated"), + ("P10", "1µm particles"), + ("P25", "2.5µm particles"), + ("durP10", "duration 1µm"), + ("durP25", "duration 2.5µm"), + ("ratioP10", "ratio 1µm in percent"), + ("ratioP25", "ratio 2.5µm in percent"), + ("door_state", "door state (open/closed)"), + ("lat", "latitude"), + ("lon", "longitude"), + ("height", "height"), + ("hdop", "horizontal dilusion of precision"), + ("timestamp", "measured timestamp"), + ("age", "measured age"), + ("satelites", "number of satelites"), + ("speed", "current speed over ground"), + ("azimuth", "track angle"), + ("noise_LA_min", "Sound level min"), + ("noise_LA_max", "Sound level max"), + ("noise_L01", "Sound level L01"), + ("noise_L95", "Sound level L95"), + ("noise_Leq", "Sound level Leq"), + ("counts_per_second", "Counts per second"), + ("counts_per_minute", "Counts per minute"), + ("radiation_msi", "MilliSievert"), + ("hv_pulses", "Count of high voltage pulses"), + ("counts", "Counts"), + ("radiation_msi", "MilliSievert"), + ("sample_time_ms", "Time per sample"), + ("co_kohm", "CO in kOhm"), + ("co_ppb", "CO in ppb"), + ("eco2", "eCO2 in ppm"), + ("co2_ppb", "CO2 in ppb"), + ("no2_kohm", "NO2 in kOhm"), + ("no2_ppb", "NO2 in ppb"), + ("ozone_ppb", "O3 in ppb"), + ("so2_ppb", "SO2 in ppb"), + ], + db_index=True, + max_length=100, + ), + ), + migrations.AddIndex( + model_name="sensorlocation", + index=models.Index(fields=["country"], name="country_idx"), + ), + migrations.AddIndex( + model_name="sensorlocation", + index=models.Index(fields=["city"], name="city_idx"), + ), + ] diff --git a/feinstaub/sensors/migrations/__init__.py b/feinstaub/sensors/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/feinstaub/sensors/models.py b/feinstaub/sensors/models.py new file mode 100644 index 0000000..eeb93d1 --- /dev/null +++ b/feinstaub/sensors/models.py @@ -0,0 +1,205 @@ +from django.contrib.auth.models import User +from django.db import models +from django_extensions.db.models import TimeStampedModel +from django.utils.timezone import now + + +class SensorType(TimeStampedModel): + uid = models.SlugField(unique=True) + name = models.CharField(max_length=1000) + manufacturer = models.CharField(max_length=1000) + description = models.CharField(max_length=10000, null=True, blank=True) + + class Meta: + ordering = ['name', ] + + def __str__(self): + return self.uid + + +class Node(TimeStampedModel): + uid = models.SlugField(unique=True) + owner = models.ForeignKey(User, on_delete=models.CASCADE) + name = models.TextField(null=True, blank=True) + description = models.TextField(null=True, blank=True) + height = models.IntegerField(null=True) + sensor_position = models.IntegerField(null=True) # 0 = no information, 1 = in backyard, 10 = just in front of the house at the street + location = models.ForeignKey("SensorLocation",on_delete=models.CASCADE) + email = models.EmailField(null=True, blank=True) + last_notify = models.DateTimeField(null=True, blank=True) + description_internal = models.TextField(null=True, blank=True) # for internal purposes, should never been provided via API / dump / ... + indoor = models.BooleanField(default=False) + inactive = models.BooleanField(default=False) + exact_location = models.BooleanField(default=False) + + class Meta: + ordering = ['uid', ] + + def __str__(self): + return self.uid + + +class Sensor(TimeStampedModel): + node = models.ForeignKey(Node, related_name="sensors",on_delete=models.CASCADE) + pin = models.CharField( + max_length=10, + default='-', + db_index=True, + help_text='differentiate the sensors on one node by giving pin used', + ) + sensor_type = models.ForeignKey(SensorType, on_delete=models.CASCADE) + description = models.TextField(null=True, blank=True) + public = models.BooleanField(default=False, db_index=True) + + class Meta: + unique_together = ('node', 'pin') + + def __str__(self): + return "{} {}".format(self.node, self.pin) + + +class SensorData(TimeStampedModel): + sensor = models.ForeignKey(Sensor, related_name="sensordatas",on_delete=models.CASCADE) + sampling_rate = models.IntegerField(null=True, blank=True, + help_text="in milliseconds") + timestamp = models.DateTimeField(default=now, db_index=True) + location = models.ForeignKey("SensorLocation",on_delete=models.CASCADE , blank=True) + software_version = models.CharField(max_length=100, default="", + help_text="sensor software version") + + class Meta(TimeStampedModel.Meta): + index_together = (('modified', ), ) + + def __str__(self): + return "{sensor} [{value_count}]".format( + sensor=self.sensor, value_count=self.sensordatavalues.count()) + + +SENSOR_TYPE_CHOICES = ( + # ppd42ns P1 -> 1µm / SDS011 P1 -> 10µm + ('P0', '1µm particles'), + ('P1', '10µm particles'), + ('P2', '2.5µm particles'), + ('durP1', 'duration 1µm'), + ('durP2', 'duration 2.5µm'), + ('ratioP1', 'ratio 1µm in percent'), + ('ratioP2', 'ratio 2.5µm in percent'), + ('samples', 'samples'), + ('interval', 'measurement interval'), + ('min_micro', 'min_micro'), + ('max_micro', 'max_micro'), + ('N05', 'count 0.5µm particles'), + ('N1', 'count 1µm particles'), + ('N25', 'count 2.5µm particles'), + ('N4', 'count 4µm particles'), + ('N10', 'count 1µm particles'), + ('TS', 'typical particle size'), + # sht10-sht15, dht11, dht22, bmp180, bme280 + ('temperature', 'Temperature'), + # sht10-sht15, dht11, dht22, bme280 + ('humidity', 'Humidity'), + # bmp180, bme280 + ('pressure', 'Pa'), + ('altitude', 'meter'), + ('pressure_sealevel', 'Pa (sealevel)'), + # + ('brightness', 'Brightness'), + # gp2y10 + ('dust_density', 'Dust density in mg/m3'), + ("vo_raw", 'Dust voltage raw'), + ("voltage", "Dust voltage calculated"), + # dsm501a + ('P10', '1µm particles'), # identical to P1 + ('P25', '2.5µm particles'), # identical to P2 + ('durP10', 'duration 1µm'), + ('durP25', 'duration 2.5µm'), + ('ratioP10', 'ratio 1µm in percent'), + ('ratioP25', 'ratio 2.5µm in percent'), + ## + ('door_state', 'door state (open/closed)'), + ## gpssensor + ('lat', 'latitude'), + ('lon', 'longitude'), + ('height', 'height'), + ('hdop', 'horizontal dilusion of precision'), + ('timestamp', 'measured timestamp'), + ('age', 'measured age'), + ('satelites', 'number of satelites'), + ('speed', 'current speed over ground'), + ('azimuth', 'track angle'), + ## noise sensor + ('noise_LA_min', 'Sound level min'), + ('noise_LA_max', 'Sound level max'), + ('noise_L01', 'Sound level L01'), + ('noise_L95', 'Sound level L95'), + ('noise_Leq', 'Sound level Leq'), + ## radiation sensor + ('counts_per_second', 'Counts per second'), + ('counts_per_minute', 'Counts per minute'), + ('radiation_msi', 'MilliSievert'), + ('hv_pulses', 'Count of high voltage pulses'), + ('counts', 'Counts'), + ('radiation_msi', 'MilliSievert'), + ('sample_time_ms', 'Time per sample'), + ##gas sensor + ('co_kohm', 'CO in kOhm'), + ('co_ppb','CO in ppb'), + ('eco2','eCO2 in ppm'), + ('co2_ppb','CO2 in ppb'), + ('no2_kohm', 'NO2 in kOhm'), + ('no2_ppb','NO2 in ppb'), + ('ozone_ppb','O3 in ppb'), + ('so2_ppb','SO2 in ppb'), + #battery monitoring + ('batt_level', 'battery percentage level'), + ('batt_voltage', 'battery voltage'), + ('batt_charging','battery boolean charging state') +) + + +class SensorDataValue(TimeStampedModel): + + sensordata = models.ForeignKey(SensorData, related_name='sensordatavalues', on_delete=models.CASCADE) + value = models.TextField(null=False) + value_type = models.CharField(max_length=100, choices=SENSOR_TYPE_CHOICES, + db_index=True) + + class Meta: + unique_together = (('sensordata', 'value_type', ), ) + + def __str__(self): + return "{sensordata}: {value} [{value_type}]".format( + sensordata=self.sensordata, + value=self.value, + value_type=self.value_type, + ) + + +class SensorLocation(TimeStampedModel): + location = models.TextField(null=True, blank=True) + latitude = models.DecimalField(max_digits=14, decimal_places=11, null=True, blank=True) + longitude = models.DecimalField(max_digits=14, decimal_places=11, null=True, blank=True) + altitude = models.DecimalField(max_digits=14, decimal_places=8, null=True, blank=True) + indoor = models.BooleanField(default=False) + street_name = models.TextField(null=True, blank=True) + street_number = models.TextField(null=True, blank=True) + postalcode = models.TextField(null=True, blank=True) + city = models.TextField(null=True, blank=True) + country = models.TextField(null=True, blank=True) + traffic_in_area = models.IntegerField(null=True) # 0 = no information, 1 = far away from traffic, 10 = lot's of traffic in area + oven_in_area = models.IntegerField(null=True) # 0 = no information, 1 = no ovens in area, 10 = it REALLY smells + industry_in_area = models.IntegerField(null=True) # 0 = no information, 1 = no industry in area, 10 = industry all around + owner = models.ForeignKey(User, null=True, blank=True, + help_text="If not set, location is public.",on_delete=models.CASCADE) + description = models.TextField(null=True, blank=True) + timestamp = models.DateTimeField(default=now) + + class Meta: + ordering = ['location', ] + indexes = [ + models.Index(fields=['country'], name='country_idx'), + models.Index(fields=['city'], name='city_idx'), + ] + + def __str__(self): + return "{location}".format(location=self.location) diff --git a/feinstaub/sensors/serializers.py b/feinstaub/sensors/serializers.py new file mode 100644 index 0000000..30f9ebe --- /dev/null +++ b/feinstaub/sensors/serializers.py @@ -0,0 +1,170 @@ +from rest_framework import exceptions, serializers + +from .models import ( + Node, + Sensor, + SensorData, + SensorDataValue, + SensorLocation, + SensorType, +) + + +class SensorDataValueSerializer(serializers.ModelSerializer): + sensordata = serializers.IntegerField(read_only=True, + source='sensordata.pk') + + class Meta: + model = SensorDataValue + fields = ('value', 'value_type', 'sensordata') + + +class SensorDataSerializer(serializers.ModelSerializer): + sensordatavalues = SensorDataValueSerializer(many=True) + sensor = serializers.IntegerField(required=False, + source='sensor.pk') + + class Meta: + model = SensorData + fields = ('sensor', 'sampling_rate', 'timestamp', 'sensordatavalues', 'software_version') + read_only = ('location') + + def create(self, validated_data): + # custom create, because of nested list of sensordatavalues + + sensordatavalues = validated_data.pop('sensordatavalues', []) + if not sensordatavalues: + raise exceptions.ValidationError('sensordatavalues was empty. Nothing to save.') + + # use sensor from authenticator + successful_authenticator = self.context['request'].successful_authenticator + if not successful_authenticator: + raise exceptions.NotAuthenticated + + node, pin = successful_authenticator.authenticate(self.context['request']) + if node.sensors.count() == 1: + sensors_qs = node.sensors.all() + else: + sensors_qs = node.sensors.filter(pin=pin) + sensor_id = sensors_qs.values_list('pk', flat=True).first() + + if not sensor_id: + raise exceptions.ValidationError('sensor could not be selected.') + validated_data['sensor_id'] = sensor_id + + # set location based on current location of sensor + validated_data['location'] = node.location + sd = SensorData.objects.create(**validated_data) + + SensorDataValue.objects.bulk_create( + SensorDataValue( + sensordata_id=sd.pk, + **value, + ) + for value in sensordatavalues + ) + + return sd + + +class NestedSensorDataValueSerializer(serializers.ModelSerializer): + class Meta: + model = SensorDataValue + fields = ('id', 'value', 'value_type') + + +class NestedSensorDataSerializer(serializers.ModelSerializer): + sensordatavalues = NestedSensorDataValueSerializer(many=True) + + class Meta: + model = SensorData + fields = ('id', 'sampling_rate', 'timestamp', 'sensordatavalues') + read_only = ('location') + + +class NestedSensorLocationSerializer(serializers.ModelSerializer): + class Meta: + model = SensorLocation + fields = ('id', "location", "indoor", "description") + + +class NestedSensorTypeSerializer(serializers.ModelSerializer): + class Meta: + model = SensorType + fields = ('id', "name", "manufacturer") + + +class NestedSensorSerializer(serializers.ModelSerializer): + sensor_type = NestedSensorTypeSerializer() + sensordatas = serializers.SerializerMethodField() + + class Meta: + model = Sensor + fields = ('id', 'description', 'pin', 'sensor_type', 'sensordatas') + + def get_sensordatas(self, obj): + sensordatas = SensorData.objects.filter(sensor=obj).order_by('-timestamp')[:2] + serializer = NestedSensorDataSerializer(instance=sensordatas, many=True) + return serializer.data + + +class NodeSerializer(serializers.ModelSerializer): + sensors = NestedSensorSerializer(many=True) + location = NestedSensorLocationSerializer() + last_data_push = serializers.SerializerMethodField() + + class Meta: + model = Node + fields = ('id', 'sensors', 'uid', 'owner', 'location', 'last_data_push') + + def get_last_data_push(self, obj): + return obj.sensors \ + .order_by('-sensordatas__timestamp') \ + .values_list('sensordatas__timestamp', flat=True) \ + .first() + + +class SensorSerializer(serializers.ModelSerializer): + sensor_type = NestedSensorTypeSerializer() + + class Meta: + model = Sensor + fields = ('id', 'description', 'pin', 'sensor_type') + + +class VerboseSensorDataSerializer(serializers.ModelSerializer): + sensordatavalues = NestedSensorDataValueSerializer(many=True) + + class Meta: + model = SensorData + fields = ('id', 'sampling_rate', 'timestamp', 'sensordatavalues', 'location', 'sensor', 'software_version') + + +# ################################################## + + +class NowSensorSerializer(serializers.ModelSerializer): + sensor_type = NestedSensorTypeSerializer() + + class Meta: + model = Sensor + fields = ('id', 'pin', 'sensor_type') + + +class NowSensorLocationSerializer(serializers.ModelSerializer): + latitude = serializers.DecimalField(max_digits=6, decimal_places=3) + longitude = serializers.DecimalField(max_digits=6, decimal_places=3) + + class Meta: + model = SensorLocation + fields = ('id', 'latitude', 'longitude') + + +class NowSerializer(serializers.ModelSerializer): + location = NowSensorLocationSerializer() + sensor = NowSensorSerializer() + sensordatavalues = NestedSensorDataValueSerializer(many=True) + + class Meta: + model = SensorData + fields = ('id', 'sampling_rate', 'timestamp', 'sensordatavalues', 'location', 'sensor') diff --git a/feinstaub/sensors/urls.py b/feinstaub/sensors/urls.py new file mode 100644 index 0000000..2c9e55c --- /dev/null +++ b/feinstaub/sensors/urls.py @@ -0,0 +1,29 @@ +# coding=utf-8 +from rest_framework import routers +# from django.conf.urls import include, url "django.conf.urls.url() was deprecated in Django 3.0, and is removed in Django 4.0+." +from django.urls import re_path as url, include +from .views import ( + NodeView, + PostSensorDataView, + SensorDataView, + SensorView, + StatisticsView, + NowView, +) +from main.views import UsersView + + +router = routers.DefaultRouter() +router.register(r'push-sensor-data', PostSensorDataView, base_name="push-sensor-data") +router.register(r'node', NodeView) +router.register(r'sensor', SensorView) +router.register(r'data', SensorDataView) +router.register(r'statistics', StatisticsView, base_name='statistics') +router.register(r'user', UsersView) +router.register(r'now', NowView) + +urlpatterns = [ + url( + regex=r'^', view=include(router.urls) + ), +] diff --git a/feinstaub/sensors/utils.py b/feinstaub/sensors/utils.py new file mode 100644 index 0000000..efc03d8 --- /dev/null +++ b/feinstaub/sensors/utils.py @@ -0,0 +1,44 @@ +from .models import ( + SensorData, + SensorDataValue, + SENSOR_TYPE_CHOICES, +) + + +def export_to_csv(): + import csv + + fieldnames = ['timestamp', 'type', 'indoor', + 'location_id', 'sampling_rate'] + fieldnames += [i for i, j in SENSOR_TYPE_CHOICES] + + with open('/tmp/data.csv', 'w', newline='') as csvfile: + csvwriter = csv.DictWriter(csvfile, fieldnames=fieldnames) + csvwriter.writeheader() + elements = SensorData.objects \ + .filter(sensor__sensor_type__name__in=[ + 'dsm501a', + 'GP2Y1010AU0F', + 'PPD42NS', + ]) \ + .values( + 'pk', 'timestamp', 'sensor__sensor_type__name', + 'location__indoor', 'location__pk', 'sampling_rate', + ) + + for element in elements: + d = { + 'timestamp': str(element['timestamp']), + 'type': element['sensor__sensor_type__name'], + 'indoor': element['location__indoor'], + 'location_id': element['location__pk'], + 'sampling_rate': element['sampling_rate'], + } + + d.update(dict( + SensorDataValue.objects + .filter(sensordata_id=element['pk']) + .values_list('value_type', 'value') + )) + + csvwriter.writerow(d) diff --git a/feinstaub/sensors/views.py b/feinstaub/sensors/views.py new file mode 100644 index 0000000..d1ed4e2 --- /dev/null +++ b/feinstaub/sensors/views.py @@ -0,0 +1,183 @@ +import datetime +import django_filters +from django.contrib.auth.mixins import LoginRequiredMixin +from django.db import models +from django.contrib.auth.models import User +from django.contrib import messages +from django.db.models import Q +from django.utils import timezone +from django.views.generic.edit import FormView + +from rest_framework import mixins, viewsets, pagination +from rest_framework.response import Response + +from .authentication import OwnerPermission, NodeUidAuthentication +from .serializers import ( + SensorDataSerializer, + NodeSerializer, + SensorSerializer, + VerboseSensorDataSerializer, + NowSerializer, +) + +from .models import ( + Node, + Sensor, + SensorData, + SensorDataValue, + SensorLocation, + SensorType, +) +from .forms import AddSensordeviceForm + + +class StandardResultsSetPagination(pagination.PageNumberPagination): + page_size = 10 + page_size_query_param = 'page_size' + max_page_size = 1000 + + +class PostSensorDataView(mixins.CreateModelMixin, + viewsets.GenericViewSet): + """ This endpoint is to POST data from the sensor to the api. + """ + authentication_classes = (NodeUidAuthentication,) + permission_classes = tuple() + serializer_class = SensorDataSerializer + queryset = SensorData.objects.all() + + +class SensorView(mixins.ListModelMixin, + mixins.RetrieveModelMixin, + viewsets.GenericViewSet): + """ This endpoint is to download sensor data from the api. + """ + permission_classes = (OwnerPermission,) + serializer_class = SensorSerializer + queryset = Sensor.objects.all() + pagination_class = StandardResultsSetPagination + + def get_queryset(self): + if self.request.user.is_authenticated: + if self.request.user.groups.filter(name="show_me_everything").exists(): + return Sensor.objects.all() + return Sensor.objects.filter(node__owner=self.request.user) + return Sensor.objects.none() + + +class SensorFilter(django_filters.FilterSet): + class Meta: + model = SensorData + fields = {"sensor": ["exact"], "timestamp": ["gte"]} + filter_overrides = { + models.DateTimeField: { + 'filter_class': django_filters.IsoDateTimeFilter, + 'extra': lambda f: { + 'lookup_expr': 'gte', + }, + }, + } + + +class SensorDataView(mixins.ListModelMixin, + mixins.RetrieveModelMixin, + viewsets.GenericViewSet): + """ This endpoint is to download sensor data from the api. + """ + permission_classes = (OwnerPermission,) + serializer_class = VerboseSensorDataSerializer + queryset = SensorData.objects.all() + pagination_class = StandardResultsSetPagination + filter_backends = (django_filters.rest_framework.DjangoFilterBackend, ) + filter_class = SensorFilter + + def get_queryset(self): + if self.request.user.is_authenticated: + if self.request.user.groups.filter(name="show_me_everything").exists(): + return SensorData.objects.all() + return SensorData.objects.filter(Q(sensor__node__owner=self.request.user) | + Q(sensor__public=True)) + return SensorData.objects.filter(sensor__public=True) + + +class NodeView(mixins.ListModelMixin, + mixins.RetrieveModelMixin, + viewsets.GenericViewSet): + """ Show all nodes belonging to authenticated user + """ + permission_classes = (OwnerPermission,) + serializer_class = NodeSerializer + queryset = Node.objects.all() + + def get_queryset(self): + if self.request.user.is_authenticated: + if self.request.user.groups.filter(name="show_me_everything").exists(): + return Node.objects.all() + return Node.objects.filter(owner=self.request.user) + return Node.objects.none() + + +class NowView(mixins.ListModelMixin, viewsets.GenericViewSet): + """ Show all sensors active in the last 5 minutes with newest value + """ + permission_classes = [] + serializer_class = NowSerializer + queryset = SensorData.objects.none() + + def get_queryset(self): + now = timezone.now() +# now = datetime.datetime(2016, 1, 1, 1, 1) + startdate = now - datetime.timedelta(minutes=5) + return SensorData.objects.filter(modified__range=[startdate, now]) + + +class StatisticsView(viewsets.ViewSet): + + def list(self, request): + stats = { + 'user': { + 'count': User.objects.count(), + }, + 'sensor': { + 'count': Sensor.objects.count(), + }, + 'sensor_data': { + 'count': SensorData.objects.count(), + }, + 'sensor_data_value': { + 'count': SensorDataValue.objects.count(), + }, + 'sensor_type': { + 'count': SensorType.objects.count(), + 'list': SensorType.objects.order_by('uid').values_list('name', flat=True) + }, + 'location': { + 'count': SensorLocation.objects.count(), + } + } + return Response(stats) + + +class AddSensordeviceView(LoginRequiredMixin, FormView): + login_url = '/admin/login/' + form_class = AddSensordeviceForm + template_name = 'addsensordevice.html' + + def form_valid(self, form): + if form.cleaned_data.get('value'): + # TODO: add logic to write all the data into the database + pass + + # thing, created = Thing.objects.get_or_create( + # field=form.cleaned_data.get('field'), + # defaults={'optional_field': form.cleaned_data.get('field2')}, + # ) + # if not created: + # messages.add_message(self.request, + # messages.ERROR, + # 'an error occurred') + messages.add_message(self.request, messages.INFO, "not implemented yet.") + return super().form_valid(form) + + # def get_success_url(self): + # return reverse('admin:xxx_create') diff --git a/requirements.txt b/requirements.txt index 92145ee..12a9c97 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,25 +1,38 @@ -django==1.11.29 #LTS +amqp==2.6.1 +asgiref==3.7.2 +billiard==3.6.4.0 +celery==4.4.7 +celery-slack==0.4.1 +certifi==2023.5.7 +charset-normalizer==3.1.0 coreapi==2.3.3 -dj-database-url==0.5.0 -timeago==1.0.10 - -flower==0.9.5 -tornado<6 -sentry-sdk==1.14.0 -celery==4.2.1 -gevent==1.2.2 -greenlet==0.4.12 -whitenoise==4.1.2 - -eradicate==1.0 -pytest==5.2.1 - -ckanapi==4.1 - -celery-slack==0.3.0 - -urllib3<1.25,>=1.21.1 #requests 2.21.0 - -django-cors-headers==3.0.2 - -geopy==2.1.0 +coreschema==0.0.4 +dj-database-url==2.0.0 +Django==4.2.1 +django-cors-headers==4.0.0 +django-extensions==3.2.1 +django-filter==23.2 +djangorestframework==3.14.0 +docopt==0.6.2 +ephem==3.7.7.1 +idna==3.4 +itypes==1.2.0 +Jinja2==3.1.2 +kombu==4.6.11 +MarkupSafe==2.1.2 +psycopg2-binary==2.9.6 +python-dateutil==2.8.2 +pytz==2023.3 +requests==2.31.0 +sentry-sdk==1.24.0 +six==1.16.0 +sqlparse==0.4.4 +timeago==1.0.16 +typing_extensions==4.6.2 +uritemplate==4.1.1 +urllib3==1.26.16 +vine==1.3.0 +whitenoise==6.4.0 +yarg==0.1.9 +gunicorn==20.1.0 +gevent \ No newline at end of file diff --git a/sensorsafrica/admin.py b/sensorsafrica/admin.py index 366d3dd..fa423f1 100644 --- a/sensorsafrica/admin.py +++ b/sensorsafrica/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin from django.utils.html import format_html -from django.conf.urls import include, url +# from django.conf.urls import include, url "django.conf.urls.url() was deprecated in Django 3.0, and is removed in Django 4.0+." +from django.urls import re_path as url, include from django.template.response import TemplateResponse from .api.models import LastActiveNodes, SensorDataStat, City from django.db.models import Q diff --git a/sensorsafrica/api/models.py b/sensorsafrica/api/models.py index 2722452..82cc01c 100644 --- a/sensorsafrica/api/models.py +++ b/sensorsafrica/api/models.py @@ -21,9 +21,9 @@ def save(self, *args, **kwargs): class SensorDataStat(TimeStampedModel): - node = models.ForeignKey(Node) - sensor = models.ForeignKey(Sensor) - location = models.ForeignKey(SensorLocation) + node = models.ForeignKey(Node,on_delete=models.CASCADE) + sensor = models.ForeignKey(Sensor,on_delete=models.CASCADE) + location = models.ForeignKey(SensorLocation,on_delete=models.CASCADE) city_slug = models.CharField(max_length=255, db_index=True, null=False, blank=False) value_type = models.CharField(max_length=255, db_index=True, null=False, blank=False) @@ -51,8 +51,8 @@ def __str__(self): class LastActiveNodes(TimeStampedModel): - node = models.ForeignKey(Node) - location = models.ForeignKey(SensorLocation) + node = models.ForeignKey(Node,on_delete=models.CASCADE) + location = models.ForeignKey(SensorLocation,on_delete=models.CASCADE) last_data_received_at = models.DateTimeField() class Meta: diff --git a/sensorsafrica/api/v2/router.py b/sensorsafrica/api/v2/router.py index 46f4bc4..2c5c64c 100644 --- a/sensorsafrica/api/v2/router.py +++ b/sensorsafrica/api/v2/router.py @@ -1,5 +1,6 @@ from rest_framework import routers -from django.conf.urls import url, include +# from django.conf.urls import include, url "django.conf.urls.url() was deprecated in Django 3.0, and is removed in Django 4.0+." +from django.urls import re_path as url, include from .views import ( CitiesView, diff --git a/sensorsafrica/migrations/0004_auto_20190509_1145.py b/sensorsafrica/migrations/0004_auto_20190509_1145.py index 112353b..9bd0991 100644 --- a/sensorsafrica/migrations/0004_auto_20190509_1145.py +++ b/sensorsafrica/migrations/0004_auto_20190509_1145.py @@ -10,7 +10,7 @@ class Migration(migrations.Migration): dependencies = [ - ('sensors', '0020_auto_20190314_1232'), + # ('sensors', '0020_auto_20190314_1232'), ('sensorsafrica', '0003_auto_20190222_1137'), ] diff --git a/sensorsafrica/migrations/0005_alter_sensordatastat_options.py b/sensorsafrica/migrations/0005_alter_sensordatastat_options.py new file mode 100644 index 0000000..b246951 --- /dev/null +++ b/sensorsafrica/migrations/0005_alter_sensordatastat_options.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.1 on 2023-05-28 11:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("sensorsafrica", "0004_auto_20190509_1145"), + ] + + operations = [ + migrations.AlterModelOptions( + name="sensordatastat", options={"get_latest_by": "modified"}, + ), + ] diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0020_auto_20190314_1232.py b/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0020_auto_20190314_1232.py deleted file mode 100644 index 739fb1a..0000000 --- a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0020_auto_20190314_1232.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.18 on 2019-03-14 12:32 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('sensors', '0019_auto_20190125_0521'), - ] - - operations = [ - migrations.AddField( - model_name='node', - name='exact_location', - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name='node', - name='inactive', - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name='node', - name='indoor', - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name='sensorlocation', - name='altitude', - field=models.DecimalField(blank=True, decimal_places=8, max_digits=14, null=True), - ), - migrations.AlterField( - model_name='sensordatavalue', - name='value', - field=models.TextField(), - ), - migrations.AlterField( - model_name='sensordatavalue', - name='value_type', - field=models.CharField(choices=[('P0', '1µm particles'), ('P1', '10µm particles'), ('P2', '2.5µm particles'), ('durP1', 'duration 1µm'), ('durP2', 'duration 2.5µm'), ('ratioP1', 'ratio 1µm in percent'), ('ratioP2', 'ratio 2.5µm in percent'), ('samples', 'samples'), ('min_micro', 'min_micro'), ('max_micro', 'max_micro'), ('temperature', 'Temperature'), ('humidity', 'Humidity'), ('pressure', 'Pa'), ('altitude', 'meter'), ('pressure_sealevel', 'Pa (sealevel)'), ('brightness', 'Brightness'), ('dust_density', 'Dust density in mg/m3'), ('vo_raw', 'Dust voltage raw'), ('voltage', 'Dust voltage calculated'), ('P10', '1µm particles'), ('P25', '2.5µm particles'), ('durP10', 'duration 1µm'), ('durP25', 'duration 2.5µm'), ('ratioP10', 'ratio 1µm in percent'), ('ratioP25', 'ratio 2.5µm in percent'), ('door_state', 'door state (open/closed)'), ('lat', 'latitude'), ('lon', 'longitude'), ('height', 'height'), ('hdop', 'horizontal dilusion of precision'), ('timestamp', 'measured timestamp'), ('age', 'measured age'), ('satelites', 'number of satelites'), ('speed', 'current speed over ground'), ('azimuth', 'track angle'), ('noise_L01', 'Sound level L01'), ('noise_L95', 'Sound level L95'), ('noise_Leq', 'Sound level Leq')], db_index=True, max_length=100), - ), - ] diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0021_auto_20210204_1106.py b/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0021_auto_20210204_1106.py deleted file mode 100644 index a1e6098..0000000 --- a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0021_auto_20210204_1106.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.29 on 2021-02-04 11:06 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('sensors', '0020_auto_20190314_1232'), - ] - - operations = [ - migrations.AlterField( - model_name='sensordatavalue', - name='value_type', - field=models.CharField(choices=[('P0', '1µm particles'), ('P1', '10µm particles'), ('P2', '2.5µm particles'), ('durP1', 'duration 1µm'), ('durP2', 'duration 2.5µm'), ('ratioP1', 'ratio 1µm in percent'), ('ratioP2', 'ratio 2.5µm in percent'), ('samples', 'samples'), ('min_micro', 'min_micro'), ('max_micro', 'max_micro'), ('temperature', 'Temperature'), ('humidity', 'Humidity'), ('pressure', 'Pa'), ('altitude', 'meter'), ('pressure_sealevel', 'Pa (sealevel)'), ('brightness', 'Brightness'), ('dust_density', 'Dust density in mg/m3'), ('vo_raw', 'Dust voltage raw'), ('voltage', 'Dust voltage calculated'), ('P10', '1µm particles'), ('P25', '2.5µm particles'), ('durP10', 'duration 1µm'), ('durP25', 'duration 2.5µm'), ('ratioP10', 'ratio 1µm in percent'), ('ratioP25', 'ratio 2.5µm in percent'), ('door_state', 'door state (open/closed)'), ('lat', 'latitude'), ('lon', 'longitude'), ('height', 'height'), ('hdop', 'horizontal dilusion of precision'), ('timestamp', 'measured timestamp'), ('age', 'measured age'), ('satelites', 'number of satelites'), ('speed', 'current speed over ground'), ('azimuth', 'track angle'), ('noise_L01', 'Sound level L01'), ('noise_L95', 'Sound level L95'), ('noise_Leq', 'Sound level Leq'), ('co_kohm', 'CO in kOhm'), ('co_ppb', 'CO in ppb'), ('eco2', 'eCO2 in ppm'), ('no2_kohm', 'NO2 in kOhm'), ('no2_ppb', 'NO2 in ppb'), ('ozone_ppb', 'O3 in ppb'), ('so2_ppb', 'SO2 in ppb')], db_index=True, max_length=100), - ), - ] diff --git a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0022_auto_20210211_2023.py b/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0022_auto_20210211_2023.py deleted file mode 100644 index 1c2c304..0000000 --- a/sensorsafrica/openstuttgart/feinstaub/sensors/migrations/0022_auto_20210211_2023.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.29 on 2021-02-11 20:23 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('sensors', '0021_auto_20210204_1106'), - ] - - operations = [ - migrations.AddIndex( - model_name='sensorlocation', - index=models.Index(fields=['country'], name='country_idx'), - ), - migrations.AddIndex( - model_name='sensorlocation', - index=models.Index(fields=['city'], name='city_idx'), - ), - ] diff --git a/sensorsafrica/settings.py b/sensorsafrica/settings.py index fcbbf17..8b577ee 100644 --- a/sensorsafrica/settings.py +++ b/sensorsafrica/settings.py @@ -81,6 +81,7 @@ "rest_framework.authentication.SessionAuthentication", ), "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",), + "DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema" } TEMPLATES = [ @@ -201,7 +202,7 @@ # Put fenstaub migrations into sensorsafrica MIGRATION_MODULES = { - "sensors": "sensorsafrica.openstuttgart.feinstaub.sensors.migrations" + "sensors": "feinstaub.sensors.migrations" } NETWORKS_OWNER = os.getenv("NETWORKS_OWNER") diff --git a/sensorsafrica/urls.py b/sensorsafrica/urls.py index 4e9f7d5..e63ae9e 100644 --- a/sensorsafrica/urls.py +++ b/sensorsafrica/urls.py @@ -13,7 +13,8 @@ 1. Import the include() function: from django.conf.urls import url, include 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ -from django.conf.urls import include, url +# from django.conf.urls import include, url "django.conf.urls.url() was deprecated in Django 3.0, and is removed in Django 4.0+." +from django.urls import re_path as url, include from django.contrib import admin from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.views.generic.base import RedirectView