From 286bdbc0de467fa9d01bac9cb675c1ea7ea5570a Mon Sep 17 00:00:00 2001 From: 21geem1bif <21geem1bif@hft-stuttgart.de> Date: Wed, 25 Jun 2025 16:18:24 +0200 Subject: [PATCH] Add API endpoints for room and sensor availability; enhance InfluxDB helper methods --- backend/app/urls.py | 4 ++ backend/app/views.py | 76 +++++++++++++++++++++++++ backend/utils/influx.py | 66 +++++++++++++++++++++ docker-compose.yaml | 8 +-- frontend/src/assets/main.css | 14 ++--- frontend/src/components/SensorLight.vue | 37 ++++++++++++ frontend/src/views/AboutView.vue | 5 +- package.json | 9 +++ 8 files changed, 201 insertions(+), 18 deletions(-) create mode 100644 frontend/src/components/SensorLight.vue create mode 100644 package.json diff --git a/backend/app/urls.py b/backend/app/urls.py index 9198df3..432ceaa 100644 --- a/backend/app/urls.py +++ b/backend/app/urls.py @@ -13,4 +13,8 @@ urlpatterns = [ path("api/room_data_csv_download", views.room_data_csv_download, name="room_data_csv_download"), path("api/get_rooms_from_building", views.get_rooms_from_building, name="get_rooms_from_building"), path("api/get_bau", views.get_bau, name="get_bau"), + path("api/room_availability", views.room_availability, name="room_availability"), + path("api/sensor_availability", views.sensor_availability, name="sensor_availability"), + path("api/all_rooms_availability", views.all_rooms_availability, name="all_rooms_availability"), + path("api/all_sensors_availability", views.all_sensors_availability, name="all_sensors_availability"), ] diff --git a/backend/app/views.py b/backend/app/views.py index fd2519f..78e679b 100644 --- a/backend/app/views.py +++ b/backend/app/views.py @@ -233,3 +233,79 @@ def room_data_csv_download(request): response = HttpResponse(csv_data, content_type="text/csv") response["Content-Disposition"] = f'attachment; filename="{room}_data.csv"' return response + +@require_http_methods(["GET"]) +def room_availability(request): + room = request.GET.get("room") + start = request.GET.get("start", "-30d") + stop = request.GET.get("stop", "now()") + + if not room: + return JsonResponse({"error": "Missing 'room' parameter"}, status=400) + + client = InfluxDBHelper( + url=os.getenv("INFLUXDB_URL"), + token=os.getenv("INFLUXDB_TOKEN"), + org=os.getenv("INFLUXDB_ORG"), + bucket=os.getenv("INFLUXDB_BUCKET"), + ) + + availability = client.has_room_sent_data(room, start, stop) + + return JsonResponse({"room": room, "available": availability}, status=200) + + +@require_http_methods(["GET"]) +def sensor_availability(request): + sensor_mac = request.GET.get("mac") + start = request.GET.get("start", "-30d") + stop = request.GET.get("stop", "now()") + + if not sensor_mac: + return JsonResponse({"error": "Missing 'mac' parameter"}, status=400) + + client = InfluxDBHelper( + url=os.getenv("INFLUXDB_URL"), + token=os.getenv("INFLUXDB_TOKEN"), + org=os.getenv("INFLUXDB_ORG"), + bucket=os.getenv("INFLUXDB_BUCKET"), + ) + + availability = client.has_sensor_sent_data(sensor_mac, start, stop) + + return JsonResponse({"sensor_mac": sensor_mac, "available": availability}, status=200) + + +@require_http_methods(["GET"]) +def all_rooms_availability(request): + start = request.GET.get("start", "-30d") + stop = request.GET.get("stop", "now()") + + client = InfluxDBHelper( + url=os.getenv("INFLUXDB_URL"), + token=os.getenv("INFLUXDB_TOKEN"), + org=os.getenv("INFLUXDB_ORG"), + bucket=os.getenv("INFLUXDB_BUCKET"), + ) + + rooms_available = client.rooms_with_available_data(start, stop) + + return JsonResponse({"available_rooms": rooms_available}, status=200) + + +@require_http_methods(["GET"]) +def all_sensors_availability(request): + start = request.GET.get("start", "-30d") + stop = request.GET.get("stop", "now()") + + client = InfluxDBHelper( + url=os.getenv("INFLUXDB_URL"), + token=os.getenv("INFLUXDB_TOKEN"), + org=os.getenv("INFLUXDB_ORG"), + bucket=os.getenv("INFLUXDB_BUCKET"), + ) + + sensors_available = client.sensors_with_available_data(start, stop) + + return JsonResponse({"available_sensors": sensors_available}, status=200) + diff --git a/backend/utils/influx.py b/backend/utils/influx.py index f34d3b7..6c2ed3d 100644 --- a/backend/utils/influx.py +++ b/backend/utils/influx.py @@ -91,5 +91,71 @@ class InfluxDBHelper: |> yield(name: "mean") ''' return self.query_api.query(org=self.org, query=query) + + # Prüft, ob ein bestimmter Raum in einem Zeitraum Daten gesendet hat + def has_room_sent_data(self, room_id: str, start: str = "-30d", stop: str = "now()") -> bool: + query = f''' + from(bucket: "{self.bucket}") + |> range(start: {start}, stop: {stop}) + |> filter(fn: (r) => r["_measurement"] == "sensor_data") + |> filter(fn: (r) => r["room"] == "{room_id}") + |> filter(fn: (r) => r["_field"] == "co2" or r["_field"] == "humidity" or r["_field"] == "temperature") + |> limit(n:1) + ''' + result = self.query_api.query(org=self.org, query=query) + return len(result) > 0 + + # Prüft, ob ein bestimmter Sensor (MAC-Adresse) Daten gesendet hat + def has_sensor_sent_data(self, mac_address: str, start: str = "-30d", stop: str = "now()") -> bool: + query = f''' + from(bucket: "{self.bucket}") + |> range(start: {start}, stop: {stop}) + |> filter(fn: (r) => r["_measurement"] == "sensor_data") + |> filter(fn: (r) => r["mac"] == "{mac_address}") + |> filter(fn: (r) => r["_field"] == "co2" or r["_field"] == "humidity" or r["_field"] == "temperature") + |> limit(n:1) + ''' + result = self.query_api.query(org=self.org, query=query) + return len(result) > 0 + # Prüft alle Räume auf Verfügbarkeit in einem Zeitraum und gibt Räume zurück, die Daten gesendet haben + def rooms_with_available_data(self, start: str = "-30d", stop: str = "now()") -> list: + query = f''' + import "influxdata/influxdb/schema" + schema.tagValues( + bucket: "{self.bucket}", + tag: "{self.TAG_ROOM}", + predicate: (r) => r["_measurement"] == "{self.MEASUREMENT_NAME}", + start: {start}, + stop: {stop} + ) + ''' + rooms_result = self.query_api.query(org=self.org, query=query) + rooms = [record.get_value() for table in rooms_result for record in table.records] + + available_rooms = [] + for room in rooms: + if self.has_room_sent_data(room, start, stop): + available_rooms.append(room) + return available_rooms + + # Prüft alle Sensoren auf Verfügbarkeit in einem Zeitraum und gibt MAC-Adressen zurück, die Daten gesendet haben + def sensors_with_available_data(self, start: str = "-30d", stop: str = "now()") -> list: + query = f''' + import "influxdata/influxdb/schema" + schema.tagValues( + bucket: "{self.bucket}", + tag: "{self.TAG_MAC}", + predicate: (r) => r["_measurement"] == "{self.MEASUREMENT_NAME}", + start: {start}, + stop: {stop} + ) + ''' + sensors_result = self.query_api.query(org=self.org, query=query) + sensors = [record.get_value() for table in sensors_result for record in table.records] + available_sensors = [] + for mac in sensors: + if self.has_sensor_sent_data(mac, start, stop): + available_sensors.append(mac) + return available_sensors diff --git a/docker-compose.yaml b/docker-compose.yaml index a31683d..66867d8 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -28,6 +28,7 @@ services: - "8000:8000" volumes: - ./backend:/app + - /app/.venv frontend: build: @@ -56,13 +57,6 @@ services: stdin_open: true tty: true - mqttx-web: - image: emqx/mqttx-web - container_name: mqttx-web - ports: - - "8081:80" # Port 80 im Container auf 8081 lokal mappen - restart: unless-stopped - influxdb2: image: influxdb:2 ports: diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index 90a6264..613d626 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -1,23 +1,21 @@ @import './base.css'; #app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; + max-width: 100%; font-weight: normal; + margin-top: 30px; + margin-left: auto; + margin-right: auto; } -a, -.green { - text-decoration: none; - color: hsla(160, 100%, 37%, 1); +a { transition: 0.4s; padding: 3px; } @media (hover: hover) { a:hover { - background-color: hsla(160, 100%, 37%, 0.2); + text-decoration: underline; } } diff --git a/frontend/src/components/SensorLight.vue b/frontend/src/components/SensorLight.vue new file mode 100644 index 0000000..76d1ac0 --- /dev/null +++ b/frontend/src/components/SensorLight.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/frontend/src/views/AboutView.vue b/frontend/src/views/AboutView.vue index 62bf062..122c301 100644 --- a/frontend/src/views/AboutView.vue +++ b/frontend/src/views/AboutView.vue @@ -16,15 +16,14 @@ messen, die Luftqualität per LED-Farbring darzustellen und Daten kabellos zu übertragen.

-

- Die Anzeige erfolgt farblich anhand der Luftqualität gemäß DIN EN 13779: +

Die Anzeige erfolgt farblich anhand der Luftqualität gemäß DIN EN 13779:

-

+

Hardware & Technik