diff --git a/backend/app/urls.py b/backend/app/urls.py index 9198df3ba09cab90c2b3c06f78cf48441d02ede6..432ceaa5a42ef7cdba10bb353d3274b1e41901fc 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 fd2519f4a6ddcd83c2c8a70cdc9f6917a8c8bb8d..78e679b9bb4faa539b0184caf86dee7e61cbf4b8 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 f34d3b75d065fc600e570e0275db5bf392516ebd..6c2ed3d715835f58921cb3dbf062e33bc6df925d 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 a31683dab409468578760ebf0fa5a7ac496e05cf..66867d8a89696d869adf3c4a1a034832902eb288 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 90a62647db1c74a8c09d3feda0c1701699dfccd0..613d626ff1f71d6202e80c2764958e7c5171caf0 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 0000000000000000000000000000000000000000..76d1ac01c11bb1425483237e1a7a9f7a06fd3ebc --- /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 62bf0624f448188357da680f94d6edf743cdf95c..122c30140d6d4a366a23b2762137e7fcbaf253ee 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