diff --git a/backend/API/app/apps.py b/backend/API/app/apps.py index 40ea550..08f32bc 100644 --- a/backend/API/app/apps.py +++ b/backend/API/app/apps.py @@ -1,4 +1,7 @@ from django.apps import AppConfig class AppConfig(AppConfig): - name = 'app' \ No newline at end of file + name = 'app' + + def ready(self): + from app.signals import data_post_save \ No newline at end of file diff --git a/backend/API/app/management/commands/startserver.py b/backend/API/app/management/commands/startserver.py index 466b4bc..3a76b59 100644 --- a/backend/API/app/management/commands/startserver.py +++ b/backend/API/app/management/commands/startserver.py @@ -1,11 +1,12 @@ import os import logging - +import threading from django.core.management.base import BaseCommand from django.core.management import call_command from project.settings.dev_settings import DEFAULT_IP, DEFAULT_PORT from app.processmqttlistenerstarter import start_process_mqtt_listener from project.loggerconfig import setup_logger +from app.threadredislistenerstarter import start_thread_redis_listener class Command(BaseCommand): help = 'Run the Django development server using environment variables for IP and port' @@ -13,7 +14,12 @@ class Command(BaseCommand): def handle(self, *args, **kwargs): if os.environ.get('RUN_MAIN'): setup_logger() - logging.getLogger('API').warning('Your are in development mode ! \nSome logs are not enabled in certain files because they slow down the server. \nCheck the relevant file and uncomment the log if necessary') + logging.getLogger('API').warning('You are in development mode ! \nSome logs are not enabled in certain files because they slow down the server. \nCheck the relevant file and uncomment the log if necessary') + + # Démarrer le listener Redis dans un thread + start_thread_redis_listener() + + # Démarrer le process MQTT listener start_process_mqtt_listener() - - call_command('runserver', f'{DEFAULT_IP}:{DEFAULT_PORT}') \ No newline at end of file + + call_command('runserver', f'{DEFAULT_IP}:{DEFAULT_PORT}') diff --git a/backend/API/app/processmqttlistenerstarter.py b/backend/API/app/processmqttlistenerstarter.py index efdf174..3ee50b8 100644 --- a/backend/API/app/processmqttlistenerstarter.py +++ b/backend/API/app/processmqttlistenerstarter.py @@ -1,7 +1,7 @@ def start_process_mqtt_listener() : import logging - from app.usecases.mqttlistener import MqttClientProcess + from app.usecases.mqtt import MqttClientProcess logger = logging.getLogger('API') @@ -15,4 +15,6 @@ def start_process_mqtt_listener() : mqtt_process_2 = MqttClientProcess(topic_2) mqtt_process_2.daemon = True mqtt_process_2.start() - logger.info("MQQT listener process on topic 2 started") \ No newline at end of file + logger.info("MQQT listener process on topic 2 started") + + return mqtt_process_1, mqtt_process_2 \ No newline at end of file diff --git a/backend/API/app/signals/__init__.py b/backend/API/app/signals/__init__.py new file mode 100644 index 0000000..c1757af --- /dev/null +++ b/backend/API/app/signals/__init__.py @@ -0,0 +1,2 @@ +from .datasignals import data_post_save +from .sensorsignals import sensor_post_save \ No newline at end of file diff --git a/backend/API/app/signals/datasignals.py b/backend/API/app/signals/datasignals.py new file mode 100644 index 0000000..bf003a1 --- /dev/null +++ b/backend/API/app/signals/datasignals.py @@ -0,0 +1,42 @@ +import logging +import json + +from django.db.models.signals import post_save +from django.dispatch import receiver +from app.models import Data, Sensor +from app.usecases.redis import redis_sender +from django.core import serializers + +logger = logging.getLogger('API') + +@receiver(post_save, sender=Data) +def data_post_save(sender, instance, **kwargs): + logger.debug("Data save, signal received") + + json_data = serializers.serialize('json', [instance]) + + data_dict = json.loads(json_data) + + fields_data = data_dict[0]['fields'] + + sensor = Sensor.objects.get(deveui=fields_data['sensor']) + + fields_data.pop('sensor', None) + + + if sensor.room is not None and sensor.batterylevel is not None and sensor.externalpowersource is not None : + redis_sender(f'Data/', {"room" : sensor.room, "data" : fields_data, "batterylevel" : sensor.batterylevel, "externalPower" : str(sensor.externalpowersource).lower() }) + logger.debug("Data/") + + redis_sender(f'Data/{sensor.room}/', fields_data) + logger.debug(f"Data/{sensor.room}/") + + if sensor.building is not None : + redis_sender(f'Data/{sensor.building}/', {"room" : sensor.room, "data" : fields_data, "batterylevel" : sensor.batterylevel, "externalPower" : str(sensor.externalpowersource).lower() }) + logger.debug(f"Data/{sensor.building}/") + + if sensor.floor is not None : + redis_sender(f'Data/{sensor.building}/{sensor.floor}/', {"room" : sensor.room, "data" : fields_data, "batterylevel" : sensor.batterylevel, "externalPower" : str(sensor.externalpowersource).lower() }) + logger.debug(f"Data/{sensor.building}/{sensor.floor}/") + + logger.debug(f"Data sent to Redis on {sensor.room}") diff --git a/backend/API/app/signals/sensorsignals.py b/backend/API/app/signals/sensorsignals.py new file mode 100644 index 0000000..fb4fc5b --- /dev/null +++ b/backend/API/app/signals/sensorsignals.py @@ -0,0 +1,26 @@ +import logging +import json + +from django.db.models.signals import post_save +from django.dispatch import receiver +from app.models import Data, Sensor +from app.usecases.redis import redis_sender +from django.core import serializers + +logger = logging.getLogger('API') + +@receiver(post_save, sender=Sensor) +def sensor_post_save(sender, instance, **kwargs): + json_data = serializers.serialize('json', [instance]) + + data_dict = json.loads(json_data) + + fields_data = data_dict[0]['fields'] + + fields_data.pop('deveui', None) + fields_data.pop('building', None) + fields_data.pop('floor', None) + fields_data.pop('batterylevel', None) + fields_data.pop('externalpowersource', None) + + redis_sender(f'Sensor/', fields_data) \ No newline at end of file diff --git a/backend/API/app/threadredislistenerstarter.py b/backend/API/app/threadredislistenerstarter.py new file mode 100644 index 0000000..fd089d4 --- /dev/null +++ b/backend/API/app/threadredislistenerstarter.py @@ -0,0 +1,10 @@ +import threading + +def start_thread_redis_listener(): + from app.usecases.redis import redis_listener + stop_event = threading.Event() + thread = threading.Thread(target=redis_listener, args=(stop_event,)) + thread.daemon = True + thread.start() + + return thread, stop_event diff --git a/backend/API/app/usecases/__init__.py b/backend/API/app/usecases/__init__.py index 0fde243..7d62879 100644 --- a/backend/API/app/usecases/__init__.py +++ b/backend/API/app/usecases/__init__.py @@ -1,2 +1 @@ -from .mqttlistener import MqttClientProcess from .createsensordata import create_sensor_data \ No newline at end of file diff --git a/backend/API/app/usecases/mqtt/__init__.py b/backend/API/app/usecases/mqtt/__init__.py new file mode 100644 index 0000000..faa6265 --- /dev/null +++ b/backend/API/app/usecases/mqtt/__init__.py @@ -0,0 +1 @@ +from .mqttlistener import MqttClientProcess \ No newline at end of file diff --git a/backend/API/app/usecases/mqttlistener.py b/backend/API/app/usecases/mqtt/mqttlistener.py similarity index 96% rename from backend/API/app/usecases/mqttlistener.py rename to backend/API/app/usecases/mqtt/mqttlistener.py index 93d9522..da1f363 100644 --- a/backend/API/app/usecases/mqttlistener.py +++ b/backend/API/app/usecases/mqtt/mqttlistener.py @@ -73,3 +73,8 @@ def reconnect(self): except Exception as e: logger.error(f"Failed to reconnect to MQTT broker. Retrying in 10 seconds... Error: {e}") time.sleep(10) + + def stop(self): + self.is_running = False + self.client.disconnect() + self.terminate() diff --git a/backend/API/app/usecases/redis/__init__.py b/backend/API/app/usecases/redis/__init__.py new file mode 100644 index 0000000..55244cc --- /dev/null +++ b/backend/API/app/usecases/redis/__init__.py @@ -0,0 +1,2 @@ +from .redissender import redis_sender +from .redislistener import redis_listener \ No newline at end of file diff --git a/backend/API/app/usecases/redis/redislistener.py b/backend/API/app/usecases/redis/redislistener.py new file mode 100644 index 0000000..74e4bef --- /dev/null +++ b/backend/API/app/usecases/redis/redislistener.py @@ -0,0 +1,44 @@ +import json +import redis +import logging + +from django_eventstream import send_event + +logger = logging.getLogger('API') + +def redis_listener(stop_event): + r = redis.Redis(host='127.0.0.1', port=6379, db=0) + listener = r.pubsub() + listener.subscribe('sse') + + for message in listener.listen(): + if stop_event.is_set(): + break + if message['type'] == 'message': + message_decode = message['data'].decode('utf-8').replace('"', "").replace("'", '"') + logger.debug(f"Message redis reçu: {message_decode}") + + message_data = json.loads(message_decode) + logger.debug(f"Message redis reçu: {message_data}") + + if 'Data' in message_data['type']: + logger.debug(f"Data received from Redis on {message_data['type']}") + formatted_message = message_data['message'] + send_event(message_data['type'], 'message', formatted_message) + logger.debug(f"Event sent on {message_data['type']}") + + elif message_data['type'] == 'Data/': + logger.debug("Data received from Redis NoRoom") + formatted_message = message_data['message'] + # formatted_message['room'] = formatted_message['room'].encode('utf-8') + logger.debug(f"Event sent on Data") + send_event(message_data['type'], 'message', formatted_message) + + elif message_data['type'] == 'Sensor/': + logger.debug("Sensor received from Redis NoRoom") + formatted_message = message_data['message'] + logger.debug(f"Event sent on Sensor") + send_event(message_data['type'], 'message', formatted_message) + + listener.unsubscribe('sse') + logger.info("Redis listener stopped") diff --git a/backend/API/app/usecases/redis/redissender.py b/backend/API/app/usecases/redis/redissender.py new file mode 100644 index 0000000..5df76d3 --- /dev/null +++ b/backend/API/app/usecases/redis/redissender.py @@ -0,0 +1,11 @@ +import redis + +def redis_sender(channel_name, message): + r = redis.Redis(host='127.0.0.1', port=6379, db=0) + r.publish('sse', str( + { + 'type': f'{channel_name}', + 'message': f'{message}' + } + ) + ) \ No newline at end of file diff --git a/backend/API/app/views/__init__.py b/backend/API/app/views/__init__.py index 0c7ae19..6a14a69 100644 --- a/backend/API/app/views/__init__.py +++ b/backend/API/app/views/__init__.py @@ -2,4 +2,5 @@ from .byroomview import ByRoomViewSet from .sensorview import SensorViewSet from .searchview import SearchViewSet -from .autocompletsearchview import AutoCompletSearchViewSet \ No newline at end of file +from .autocompletsearchview import AutoCompletSearchViewSet +from .eventsdescription import events_description \ No newline at end of file diff --git a/backend/API/app/views/byroomview.py b/backend/API/app/views/byroomview.py index 0bfbadb..93a752e 100644 --- a/backend/API/app/views/byroomview.py +++ b/backend/API/app/views/byroomview.py @@ -37,14 +37,27 @@ def filter_sensor_data(self, sensor, date_from, date_to, depth, last_data=None): queryset = queryset.filter(time__gte=date_from) if date_to: queryset = queryset.filter(time__lte=date_to) - - # Appliquer le filtrage pour la profondeur et les dernières données + if depth > 0: filtered_data = list(queryset) - sensor.filtered_data = filtered_data[-int(last_data):] if last_data else filtered_data + # Gérer spécifiquement last_data égal à 0 + if last_data is not None: + if int(last_data) == 0: + sensor.filtered_data = [] + else: + sensor.filtered_data = filtered_data[-int(last_data):] + else: + sensor.filtered_data = filtered_data sensor.sensor = sensor elif depth == 0: - sensor.data_ids = [data.id for data in queryset][-int(last_data):] if last_data else [data.id for data in queryset] + # Gérer spécifiquement last_data égal à 0 + if last_data is not None: + if int(last_data) == 0: + sensor.data_ids = [] + else: + sensor.data_ids = [data.id for data in queryset][-int(last_data):] + else: + sensor.data_ids = [data.id for data in queryset] sensor.sensor_id = sensor.deveui return sensor diff --git a/backend/API/app/views/eventsdescription.py b/backend/API/app/views/eventsdescription.py new file mode 100644 index 0000000..0ca6bca --- /dev/null +++ b/backend/API/app/views/eventsdescription.py @@ -0,0 +1,9 @@ +from django.http import JsonResponse + +def events_description(request): + return JsonResponse({ + "endpoints": { + "Sensor": "/Events/Sensor/", + "Data": "/Events/Data/" + } + }) \ No newline at end of file diff --git a/backend/API/gunicorn_config.py b/backend/API/gunicorn_config.py index 66453b7..8678ce0 100644 --- a/backend/API/gunicorn_config.py +++ b/backend/API/gunicorn_config.py @@ -2,22 +2,44 @@ import os from project.loggerconfig import setup_logger, setup_gunicorn_loggers -from project.settings.prod_settings import DEFAULT_IP, DEFAULT_PORT, CERTFILE, KEYFILE +from project.settings.prod_settings import DEFAULT_IP, DEFAULT_PORT#, CERTFILE, KEYFILE from app.processmqttlistenerstarter import start_process_mqtt_listener +from app.threadredislistenerstarter import start_thread_redis_listener from django.core.wsgi import get_wsgi_application bind = f"{DEFAULT_IP}:{DEFAULT_PORT}" custom_logger = setup_logger() -workers = multiprocessing.cpu_count() * 2 + 1 -certfile = CERTFILE -keyfile = KEYFILE +workers = multiprocessing.cpu_count() * 2 +# certfile = CERTFILE +# keyfile = KEYFILE def on_starting(server): setup_gunicorn_loggers(custom_logger) + get_wsgi_application() def when_ready(server): custom_logger.info("Starting MQTT listener process") os.environ.get("DJANGO_SETTINGS_MODULE") - get_wsgi_application() - start_process_mqtt_listener() \ No newline at end of file + mqttlisteners = start_process_mqtt_listener() + server.mqttlisteners = mqttlisteners + +def post_fork(server, worker): + worker_id = worker.pid + custom_logger.info(f"Starting Redis listener in worker {worker_id}") + thread, stop_event = start_thread_redis_listener() + worker.redis_listener_thread = thread + worker.redis_stop_event = stop_event + custom_logger.info(f"Redis listener running in worker {worker_id}") + +def worker_exit(server, worker): + worker.redis_stop_event.set() + worker.redis_listener_thread.join() + custom_logger.info(f"Redis listener stopped in worker {worker.pid}") + + +def on_exit(server): + custom_logger.info("Stopping MQTT listener process") + for listener in server.mqttlisteners: + listener.stop() + custom_logger.info("MQTT listener process stopped") \ No newline at end of file diff --git a/backend/API/project/asgi.py b/backend/API/project/asgi.py index 087ff11..4ea51da 100644 --- a/backend/API/project/asgi.py +++ b/backend/API/project/asgi.py @@ -8,10 +8,43 @@ """ import os +import django_eventstream.routing +from django.urls import path, re_path +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.auth import AuthMiddlewareStack from django.core.asgi import get_asgi_application -from app.processmqttlistenerstarter import start_process_mqtt_listener os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') -application = get_asgi_application() \ No newline at end of file +application = ProtocolTypeRouter({ + 'http': URLRouter([ + path('Events/Sensor/', AuthMiddlewareStack( + URLRouter(django_eventstream.routing.urlpatterns) + ), {'format-channels': ['Sensor/']}), + + path('Events/Data/', AuthMiddlewareStack( + URLRouter(django_eventstream.routing.urlpatterns) + ), {'format-channels': ['Data/']}), + + re_path(r'^Events/Data/(?P[\w\-]+)/$', + AuthMiddlewareStack( + URLRouter( + django_eventstream.routing.urlpatterns + ) + ), + {'format-channels': ['Data/{RoomOrBuilding}/']} + ), + + re_path(r'^Events/Data/(?P\w+)/(?P\w+)/$', + AuthMiddlewareStack( + URLRouter( + django_eventstream.routing.urlpatterns + ) + ), + {'format-channels': ['Data/{Building}/{Floor}/']} + ), + + re_path(r'', get_asgi_application()), + ]), +}) \ No newline at end of file diff --git a/backend/API/project/settings/base_settings.py b/backend/API/project/settings/base_settings.py index f7a6bec..bb020b6 100644 --- a/backend/API/project/settings/base_settings.py +++ b/backend/API/project/settings/base_settings.py @@ -17,10 +17,14 @@ 'corsheaders', 'timescale', 'django_extensions', + 'channels', 'rest_framework', 'app', ] + +ASGI_APPLICATION = 'project.asgi.application' + MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', @@ -30,6 +34,7 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django_grip.GripMiddleware', ] ROOT_URLCONF = 'project.urls' @@ -97,4 +102,6 @@ def format(self, record): record.levelname = levelname_color return logging.Formatter.format(self, record) -PATH_TO_LOG_FILE = os.path.join(BASE_DIR, 'api.log') \ No newline at end of file +PATH_TO_LOG_FILE = os.path.join(BASE_DIR, 'api.log') + +EVENTSTREAM_ALLOW_ORIGIN = "*" \ No newline at end of file diff --git a/backend/API/project/settings/dev_settings.py b/backend/API/project/settings/dev_settings.py index 7f73ff9..51ce557 100644 --- a/backend/API/project/settings/dev_settings.py +++ b/backend/API/project/settings/dev_settings.py @@ -7,4 +7,4 @@ ALLOWED_HOSTS = ['localhost', 'localhost:8080', 'localhost:5173', "http://localhost:8080", "http://localhost:5173", "http://localhost:5500", "localhost:5500" ] -LOGGING_LEVEL = logging.DEBUG if DEBUG else logging.INFO \ No newline at end of file +LOGGING_LEVEL = logging.DEBUG if DEBUG else logging.INFO diff --git a/backend/API/project/settings/prod_settings.py b/backend/API/project/settings/prod_settings.py index 1f9225b..170275d 100644 --- a/backend/API/project/settings/prod_settings.py +++ b/backend/API/project/settings/prod_settings.py @@ -22,5 +22,5 @@ ) } -CERTFILE = os.environ.get('CERTFILE') -KEYFILE = os.environ.get('KEYFILE') \ No newline at end of file +# CERTFILE = os.environ.get('CERTFILE') +# KEYFILE = os.environ.get('KEYFILE') \ No newline at end of file diff --git a/backend/API/project/urls.py b/backend/API/project/urls.py index 00d7d12..f58fcca 100644 --- a/backend/API/project/urls.py +++ b/backend/API/project/urls.py @@ -13,6 +13,8 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +import django_eventstream + from django.urls import include, re_path from deepserializer import DeepViewSet from rest_framework import routers @@ -20,7 +22,8 @@ from django.conf import settings from app.models import Data, Sensor from django.urls import path -from app.views import DataViewSet, ByRoomViewSet, SensorViewSet, SearchViewSet, AutoCompletSearchViewSet +from app.views import DataViewSet, ByRoomViewSet, SensorViewSet, SearchViewSet, AutoCompletSearchViewSet, events_description + router = routers.DefaultRouter() DeepViewSet.init_router(router, [ @@ -34,5 +37,6 @@ urlpatterns = [ path('AutoCompletSearch/', AutoCompletSearchViewSet.as_view(), name='AutoCompletSearch'), + path('Events/', events_description, name='events_description'), re_path(r'', include(router.urls)), -]+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) \ No newline at end of file +] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/backend/Dockerfile.Dev b/backend/Dockerfile.Dev index 369da61..bb3284b 100644 --- a/backend/Dockerfile.Dev +++ b/backend/Dockerfile.Dev @@ -14,11 +14,17 @@ COPY ./requirements.txt /API/ # Installe les dépendances Python RUN pip install --no-cache-dir -r requirements.txt +# Installer redis et suppirmer le cache +RUN apt-get update && apt-get install -y redis-server && apt-get clean + +RUN rm -rf /var/lib/apt/lists/* + # Execute pour debug si nécessaire # CMD [ "tail", "-f", "/dev/null" ] # Utiliser supervisord pour démarrer les services -CMD python manage.py wait_for_db \ +CMD redis-server --daemonize yes \ + && python manage.py wait_for_db \ && python manage.py makemigrations \ && python manage.py migrate \ && tail -f /dev/null \ No newline at end of file diff --git a/backend/Dockerfile.Prod b/backend/Dockerfile.Prod index cdb8801..a129e9a 100644 --- a/backend/Dockerfile.Prod +++ b/backend/Dockerfile.Prod @@ -22,7 +22,7 @@ RUN pip install --no-cache-dir -r requirements.txt # Ajouter uvicorn et gunicorn RUN pip install uvicorn gunicorn -RUN apt-get update && apt-get install -y openssl && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y openssl redis-server && apt-get clean && rm -rf /var/lib/apt/lists/* # Création d'un répertoire pour le certificat et la clé RUN mkdir -p /certificates @@ -37,8 +37,10 @@ RUN openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout /certificates/localhost.key \ -out /certificates/localhost.crt -CMD python manage.py wait_for_db \ - && python manage.py makemigrations \ - && python manage.py migrate \ - && gunicorn project.asgi:application -k uvicorn.workers.UvicornWorker -c gunicorn_config.py +COPY ./entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh new file mode 100644 index 0000000..e6fab6e --- /dev/null +++ b/backend/entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# Démarrer Redis en arrière-plan +redis-server --daemonize yes + +# Attendre que la base de données soit prête +python manage.py wait_for_db + +# Exécuter les migrations +python manage.py makemigrations +python manage.py migrate + +# Démarrer Gunicorn avec Uvicorn +exec gunicorn project.asgi:application -k uvicorn.workers.UvicornWorker -c gunicorn_config.py diff --git a/backend/requirements.txt b/backend/requirements.txt index f41ebdb..e4b0ce8 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -7,4 +7,8 @@ python-dateutil django-cors-headers uuid dj_database_url -django_extensions \ No newline at end of file +django_extensions +django-eventstream +channels==3.0.5 +django-grip +redis \ No newline at end of file diff --git a/deploiement-prod/.env b/deploiement-prod/.env index 02203da..d3ea640 100644 --- a/deploiement-prod/.env +++ b/deploiement-prod/.env @@ -14,7 +14,7 @@ DJANGO_PORT=8000 TZ=Europe/Paris # Env variable for API url -API_URL=https://localhost:8000 +API_URL=http://localhost:8000 # Définition des paramètres de l'application Django à utiliser DJANGO_SETTINGS_MODULE=project.settings.prod_settings diff --git a/frontend/front-vue/package-lock.json b/frontend/front-vue/package-lock.json index f4e4c64..7b03752 100644 --- a/frontend/front-vue/package-lock.json +++ b/frontend/front-vue/package-lock.json @@ -12,7 +12,8 @@ "chartjs-adapter-date-fns": "^3.0.0", "date-fns": "^3.2.0", "vue": "^3.3.11", - "vue-chartjs": "^5.3.0" + "vue-chartjs": "^5.3.0", + "vue-sse": "^2.5.2" }, "devDependencies": { "@vitejs/plugin-vue": "^4.5.2", @@ -871,6 +872,11 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, + "node_modules/event-source-polyfill": { + "version": "1.0.31", + "resolved": "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.31.tgz", + "integrity": "sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==" + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -1338,6 +1344,17 @@ "chart.js": "^4.1.1", "vue": "^3.0.0-0 || ^2.7.0" } + }, + "node_modules/vue-sse": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/vue-sse/-/vue-sse-2.5.2.tgz", + "integrity": "sha512-HJu2JGEld2ez1Ko9ZhuTCTe563eq4qXESg1cjlS99pwMQ8COE5fAAtoJ4Lw/dr+K+1tLLBiFvvb98364YWZ2UQ==", + "dependencies": { + "event-source-polyfill": "^1.0.22" + }, + "peerDependencies": { + "vue": "^2.0.0 || ^3.0.0" + } } }, "dependencies": { @@ -1840,6 +1857,11 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, + "event-source-polyfill": { + "version": "1.0.31", + "resolved": "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.31.tgz", + "integrity": "sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==" + }, "fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -2114,6 +2136,14 @@ "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.0.tgz", "integrity": "sha512-8XqX0JU8vFZ+WA2/knz4z3ThClduni2Nm0BMe2u0mXgTfd9pXrmJ07QBI+WAij5P/aPmPMX54HCE1seWL37ZdQ==", "requires": {} + }, + "vue-sse": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/vue-sse/-/vue-sse-2.5.2.tgz", + "integrity": "sha512-HJu2JGEld2ez1Ko9ZhuTCTe563eq4qXESg1cjlS99pwMQ8COE5fAAtoJ4Lw/dr+K+1tLLBiFvvb98364YWZ2UQ==", + "requires": { + "event-source-polyfill": "^1.0.22" + } } } } diff --git a/frontend/front-vue/package.json b/frontend/front-vue/package.json index 5e3ac22..fceddd3 100644 --- a/frontend/front-vue/package.json +++ b/frontend/front-vue/package.json @@ -13,7 +13,8 @@ "chartjs-adapter-date-fns": "^3.0.0", "date-fns": "^3.2.0", "vue": "^3.3.11", - "vue-chartjs": "^5.3.0" + "vue-chartjs": "^5.3.0", + "vue-sse": "^2.5.2" }, "devDependencies": { "@vitejs/plugin-vue": "^4.5.2", diff --git a/frontend/front-vue/src/App.vue b/frontend/front-vue/src/App.vue index 2f13acb..321e92f 100644 --- a/frontend/front-vue/src/App.vue +++ b/frontend/front-vue/src/App.vue @@ -12,6 +12,7 @@ import Header from "@/components/Header.vue"; import RoomDetail from "@/components/roomDetail/roomDetail.vue"; import ListeSalles from "@/components/ListeSalles.vue"; + export default { components: { RoomDetail, diff --git a/frontend/front-vue/src/components/ListeSalles.vue b/frontend/front-vue/src/components/ListeSalles.vue index 4e88a1d..d9d515f 100644 --- a/frontend/front-vue/src/components/ListeSalles.vue +++ b/frontend/front-vue/src/components/ListeSalles.vue @@ -1,4 +1,8 @@ @@ -91,5 +140,11 @@ import loadApiConfig from '../utils/api.js'; .field-name { font-weight: bold; } + + .title-container { + display: flex; + justify-content: center; + margin-bottom: 2rem; + } \ No newline at end of file diff --git a/frontend/front-vue/src/components/batiments/A/BatA.vue b/frontend/front-vue/src/components/batiments/A/BatA.vue new file mode 100644 index 0000000..ad49f66 --- /dev/null +++ b/frontend/front-vue/src/components/batiments/A/BatA.vue @@ -0,0 +1,47 @@ + + + + + \ No newline at end of file diff --git a/frontend/front-vue/src/components/batiments/A/BatABibAmphis.vue b/frontend/front-vue/src/components/batiments/A/BatABibAmphis.vue deleted file mode 100644 index cef6e9e..0000000 --- a/frontend/front-vue/src/components/batiments/A/BatABibAmphis.vue +++ /dev/null @@ -1,304 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/frontend/front-vue/src/components/batiments/B/BatB.vue b/frontend/front-vue/src/components/batiments/B/BatB.vue index e85429a..bd2dd9e 100644 --- a/frontend/front-vue/src/components/batiments/B/BatB.vue +++ b/frontend/front-vue/src/components/batiments/B/BatB.vue @@ -110,14 +110,14 @@ - - + + diff --git a/frontend/front-vue/src/components/batiments/B/etages/BatBEtage1.vue b/frontend/front-vue/src/components/batiments/B/etages/BatBEtage1.vue new file mode 100644 index 0000000..19e20b4 --- /dev/null +++ b/frontend/front-vue/src/components/batiments/B/etages/BatBEtage1.vue @@ -0,0 +1,43 @@ + + + + + \ No newline at end of file diff --git a/frontend/front-vue/src/components/batiments/B/etages/BatBEtage2.vue b/frontend/front-vue/src/components/batiments/B/etages/BatBEtage2.vue new file mode 100644 index 0000000..6cf4e2d --- /dev/null +++ b/frontend/front-vue/src/components/batiments/B/etages/BatBEtage2.vue @@ -0,0 +1,63 @@ + + + + + \ No newline at end of file diff --git a/frontend/front-vue/src/components/batiments/B/etages/BatBRdc.vue b/frontend/front-vue/src/components/batiments/B/etages/BatBRdc.vue index 8a6d20f..dc67b35 100644 --- a/frontend/front-vue/src/components/batiments/B/etages/BatBRdc.vue +++ b/frontend/front-vue/src/components/batiments/B/etages/BatBRdc.vue @@ -1,291 +1,37 @@ - - - + - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/front-vue/src/components/batiments/B/etages/BatBetage1.vue b/frontend/front-vue/src/components/batiments/B/etages/BatBetage1.vue deleted file mode 100644 index 35272c1..0000000 --- a/frontend/front-vue/src/components/batiments/B/etages/BatBetage1.vue +++ /dev/null @@ -1,437 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/frontend/front-vue/src/components/batiments/B/etages/BatBetage2.vue b/frontend/front-vue/src/components/batiments/B/etages/BatBetage2.vue deleted file mode 100644 index b072fad..0000000 --- a/frontend/front-vue/src/components/batiments/B/etages/BatBetage2.vue +++ /dev/null @@ -1,648 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/frontend/front-vue/src/components/batiments/E/etages/BatERdc.vue b/frontend/front-vue/src/components/batiments/E/etages/BatERdc.vue index ca60d0c..8fd591d 100644 --- a/frontend/front-vue/src/components/batiments/E/etages/BatERdc.vue +++ b/frontend/front-vue/src/components/batiments/E/etages/BatERdc.vue @@ -1,289 +1,34 @@ - - - + \ No newline at end of file diff --git a/frontend/front-vue/src/components/batiments/E/etages/BatEetage1.vue b/frontend/front-vue/src/components/batiments/E/etages/BatEetage1.vue index 2d8e10d..710e8ac 100644 --- a/frontend/front-vue/src/components/batiments/E/etages/BatEetage1.vue +++ b/frontend/front-vue/src/components/batiments/E/etages/BatEetage1.vue @@ -1,283 +1,32 @@ - - - - - - \ No newline at end of file diff --git a/frontend/front-vue/src/components/batiments/E/etages/BatEetage2.vue b/frontend/front-vue/src/components/batiments/E/etages/BatEetage2.vue index 0c691c8..3e5a260 100644 --- a/frontend/front-vue/src/components/batiments/E/etages/BatEetage2.vue +++ b/frontend/front-vue/src/components/batiments/E/etages/BatEetage2.vue @@ -1,297 +1,43 @@ - - - + \ No newline at end of file diff --git a/frontend/front-vue/src/components/batiments/FullIut.vue b/frontend/front-vue/src/components/batiments/FullIut.vue index cb76799..7b8d042 100644 --- a/frontend/front-vue/src/components/batiments/FullIut.vue +++ b/frontend/front-vue/src/components/batiments/FullIut.vue @@ -83,14 +83,14 @@ - + diff --git a/frontend/front-vue/src/components/batiments/utils/dataScale.vue b/frontend/front-vue/src/components/batiments/utils/dataScale.vue index d3fa7e9..9e50ad8 100644 --- a/frontend/front-vue/src/components/batiments/utils/dataScale.vue +++ b/frontend/front-vue/src/components/batiments/utils/dataScale.vue @@ -1,8 +1,9 @@ + + + \ No newline at end of file diff --git a/frontend/front-vue/src/components/batiments/utils/selector.vue b/frontend/front-vue/src/components/batiments/utils/selector.vue index 3bea7d8..e8802e3 100644 --- a/frontend/front-vue/src/components/batiments/utils/selector.vue +++ b/frontend/front-vue/src/components/batiments/utils/selector.vue @@ -6,8 +6,9 @@ + - + @@ -43,15 +44,17 @@ unit: { type: String, required: true - } + }, }, data() { return { selectedOption: 'activity', + battery: false }; }, methods: { updateSelectedOption(event) { + this.battery = event.target.value === 'battery'; this.$emit('updateSelectedOption', event.target.value); this.selectedOption = event.target.value; } diff --git a/frontend/front-vue/src/components/roomDetail/gauge.vue b/frontend/front-vue/src/components/roomDetail/gauge.vue index fcd5283..685d572 100644 --- a/frontend/front-vue/src/components/roomDetail/gauge.vue +++ b/frontend/front-vue/src/components/roomDetail/gauge.vue @@ -54,8 +54,16 @@ export default { mounted() { this.createChart(); }, + beforeDestroy() { + this.chart.destroy(); + }, methods: { createChart() { + + if (this.chart) { + this.chart.destroy(); + } + const centerTextPlugin = { id: 'centerText', afterDraw: (chart) => { @@ -128,7 +136,7 @@ export default { }; const ctx = document.getElementById(this.uniqueid).getContext('2d'); - new Chart(ctx, options); + this.chart = new Chart(ctx, options); } }, watch: { @@ -144,7 +152,8 @@ export default { }, data() { return { - uniqueid: 'gauge-chart-' + crypto.randomUUID() + uniqueid: 'gauge-chart-' + crypto.randomUUID(), + chart: null }; } }; diff --git a/frontend/front-vue/src/components/roomDetail/roomDetail.vue b/frontend/front-vue/src/components/roomDetail/roomDetail.vue index 4cf7636..5d993bd 100644 --- a/frontend/front-vue/src/components/roomDetail/roomDetail.vue +++ b/frontend/front-vue/src/components/roomDetail/roomDetail.vue @@ -77,6 +77,7 @@ ChartJS.register( Legend ) +let sseClient; export default { @@ -123,6 +124,9 @@ export default { sensorBuilding: '', sensorFloor: '', sensorExternalPower: false, + duration: 60*24, + from: null, + to: null, chartOptions: { type: "line", @@ -163,6 +167,7 @@ export default { }, async updateLineMinutes(minutes){ let days = minutes / 60 / 24; + this.duration = minutes; // Calculate the date with the offset from now in minutes using thit format %Y-%m-%dT%H:%M:%S.%f %z let from = new Date(Date.now() - minutes * 60 * 1000).toISOString().slice(0, -5); @@ -174,6 +179,9 @@ export default { async updateLineFromTo(){ let from = this.$refs.from.value; let to = this.$refs.to.value; + this.duration = null; + this.from = from; + this.to = to; // apply local offset to make it utc from = new Date(new Date(from) - new Date(Date.now()).getTimezoneOffset()).toISOString().slice(0,-5); @@ -331,8 +339,61 @@ export default { try { const apiIp = await loadApiConfig(); this.apiBaseUrl = apiIp; - await this.loadData(60*24); - // Reste du code + await this.loadData(this.duration); + + if (sseClient){ + sseClient.disconnect(); + sseClient.off() + } + + sseClient = this.$sse.create({ + url: `${this.apiBaseUrl}/Events/Data/${this.room}/`, + format: 'json', + }) + + + sseClient.on('error', (e) => { + console.error('lost connection or failed to parse!', e); + }); + + sseClient.on('message', (message, lastEventId) => { + this.temp = message.temperature; + this.hum = message.humidity; + this.co2 = message.co2; + + this.lastDataReceived = new Date(message.time).toLocaleString(undefined, { + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + + // If the graph is in "duration" mode, we add the new data to the graph + if (this.duration) { + this.updateLineMinutes(this.duration); + } + + }); + + sseClient.connect() + .then(sse => { + + // Unsubscribes from event-less messages after 7 seconds + setTimeout(() => { + sseClient.off(); + }, 7000); + + // Unsubscribes from chat messages after 14 seconds + setTimeout(() => { + sseClient.off(); + }, 14000); + }) + .catch((err) => { + // When this error is caught, it means the initial connection to the + // events server failed. No automatic attempts to reconnect will be made. + console.error('Failed to connect to server', err); + }); } catch (error) { console.error("Error while loading API config:", error); } @@ -349,6 +410,11 @@ export default { behavior: "smooth" }); }, + beforeDestroy() { + sseClient.disconnect(); + sseClient.off() + }, + }; diff --git a/frontend/front-vue/src/main.js b/frontend/front-vue/src/main.js index 0ac3a5f..bb13a23 100644 --- a/frontend/front-vue/src/main.js +++ b/frontend/front-vue/src/main.js @@ -2,5 +2,11 @@ import './assets/main.css' import { createApp } from 'vue' import App from './App.vue' +import VueSSE from 'vue-sse' -createApp(App).mount('#app') + +const app = createApp(App) + +app.use(VueSSE) + +app.mount('#app') \ No newline at end of file