From 74c4c4059700dc5756d341631a5a0a234f34ce42 Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Wed, 25 Feb 2026 10:53:40 -0500 Subject: [PATCH 01/31] First commit --- IOTMonitoringServer/settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/IOTMonitoringServer/settings.py b/IOTMonitoringServer/settings.py index 114ccc2f..aa2d15bc 100644 --- a/IOTMonitoringServer/settings.py +++ b/IOTMonitoringServer/settings.py @@ -27,7 +27,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ["localhost", "ip.maquina.visualizador"] +ALLOWED_HOSTS = ["localhost", "3.86.155.37"] # Application definition @@ -96,7 +96,7 @@ "NAME": "iot_data", # Nombre de la base de datos "USER": "dbadmin", # Nombre de usuario "PASSWORD": "uniandesIOT1234*", # Contraseña - "HOST": "ip.maquina.db", # Dirección IP de la base de datos + "HOST": "100.53.99.10", # Dirección IP de la base de datos "PORT": "", # Puerto de la base de datos } } @@ -156,7 +156,7 @@ CRISPY_TEMPLATE_PACK = 'bootstrap4' # Dirección del bróker MQTT -MQTT_HOST = "ip.maquina.mqtt" +MQTT_HOST = "34.238.233.176" # Puerto del bróker MQTT MQTT_PORT = 8082 From 1c256b32e3556d39910e6f2ab5e296886f5686a8 Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Wed, 25 Feb 2026 14:50:30 -0500 Subject: [PATCH 02/31] Second commit --- IOTMonitoringServer/settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/IOTMonitoringServer/settings.py b/IOTMonitoringServer/settings.py index aa2d15bc..0274cd5a 100644 --- a/IOTMonitoringServer/settings.py +++ b/IOTMonitoringServer/settings.py @@ -27,7 +27,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ["localhost", "3.86.155.37"] +ALLOWED_HOSTS = ["localhost", "52.55.153.138"] # Application definition @@ -96,7 +96,7 @@ "NAME": "iot_data", # Nombre de la base de datos "USER": "dbadmin", # Nombre de usuario "PASSWORD": "uniandesIOT1234*", # Contraseña - "HOST": "100.53.99.10", # Dirección IP de la base de datos + "HOST": "3.95.244.217", # Dirección IP de la base de datos "PORT": "", # Puerto de la base de datos } } @@ -156,7 +156,7 @@ CRISPY_TEMPLATE_PACK = 'bootstrap4' # Dirección del bróker MQTT -MQTT_HOST = "34.238.233.176" +MQTT_HOST = "54.211.59.63" # Puerto del bróker MQTT MQTT_PORT = 8082 From b697ed3570e4b7ba0d625e6e4932cafb57bae23f Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Wed, 25 Feb 2026 15:46:10 -0500 Subject: [PATCH 03/31] HOST BD --- IOTMonitoringServer/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IOTMonitoringServer/settings.py b/IOTMonitoringServer/settings.py index 0274cd5a..95f87fc4 100644 --- a/IOTMonitoringServer/settings.py +++ b/IOTMonitoringServer/settings.py @@ -96,7 +96,7 @@ "NAME": "iot_data", # Nombre de la base de datos "USER": "dbadmin", # Nombre de usuario "PASSWORD": "uniandesIOT1234*", # Contraseña - "HOST": "3.95.244.217", # Dirección IP de la base de datos + "HOST": "10.0.84.122", # Dirección IP de la base de datos "PORT": "", # Puerto de la base de datos } } From 4b5d59771591b8acb4a7784665e07a1c9a8800af Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Wed, 25 Feb 2026 15:47:08 -0500 Subject: [PATCH 04/31] HOST BD Public IP --- IOTMonitoringServer/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IOTMonitoringServer/settings.py b/IOTMonitoringServer/settings.py index 95f87fc4..0274cd5a 100644 --- a/IOTMonitoringServer/settings.py +++ b/IOTMonitoringServer/settings.py @@ -96,7 +96,7 @@ "NAME": "iot_data", # Nombre de la base de datos "USER": "dbadmin", # Nombre de usuario "PASSWORD": "uniandesIOT1234*", # Contraseña - "HOST": "10.0.84.122", # Dirección IP de la base de datos + "HOST": "3.95.244.217", # Dirección IP de la base de datos "PORT": "", # Puerto de la base de datos } } From b26b62a6ccf661f1c8d45fc3eafef4869e68d9be Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Wed, 25 Feb 2026 17:46:58 -0500 Subject: [PATCH 05/31] Ajustar mqtt --- receiver/mqtt.py | 81 ++++++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/receiver/mqtt.py b/receiver/mqtt.py index 15fd8136..56972ee4 100644 --- a/receiver/mqtt.py +++ b/receiver/mqtt.py @@ -1,35 +1,22 @@ from datetime import datetime from . import utils import json -import os import ssl import paho.mqtt.client as mqtt from django.conf import settings def on_message(client: mqtt.Client, userdata, message: mqtt.MQTTMessage): - ''' + """ Función que se ejecuta cada que llega un mensaje al tópico. - Recibe el mensaje con formato: - { - "variable1": mediciónVariable1, - "variable2": mediciónVariable2 - } - en un tópico con formato: - pais/estado/ciudad/usuario - ej: colombia/cundinamarca/cajica/ja.avelino - Si el tópico tiene la forma de: - pais/estado/ciudad/usuario/mensaje - se salta el procesamiento pues el mensaje es para el dispositivo de medición. - A partir de esos datos almacena la medición en el sistema. - ''' + """ try: time = datetime.now() payload = message.payload.decode("utf-8") - print("payload: " + payload) + print("Payload recibido:", payload) + payloadJson = json.loads(payload) - country, state, city, user = utils.get_topic_data( - message.topic) + country, state, city, user = utils.get_topic_data(message.topic) user_obj = utils.get_user(user) location_obj = utils.get_or_create_location(city, state, country) @@ -39,42 +26,62 @@ def on_message(client: mqtt.Client, userdata, message: mqtt.MQTTMessage): unit = utils.get_units(str(variable).lower()) variable_obj = utils.get_or_create_measurement(variable, unit) sensor_obj = utils.get_or_create_station(user_obj, location_obj) + utils.create_data( - float(payloadJson[measure]), sensor_obj, variable_obj, time) + float(payloadJson[measure]), + sensor_obj, + variable_obj, + time + ) except Exception as e: - print('Ocurrió un error procesando el paquete MQTT', e) + print("❌ Error procesando mensaje MQTT:", e) def on_connect(client, userdata, flags, rc): - print("Suscribiendo al tópico: " + settings.TOPIC) - client.subscribe(settings.TOPIC) - print("Servicio de recepcion de datos iniciado") + if rc == 0: + print("✅ Conectado al broker") + print("Suscribiendo al tópico:", settings.TOPIC) + client.subscribe(settings.TOPIC) + print("Servicio de recepción de datos iniciado") + else: + print("❌ Error de conexión:", mqtt.connack_string(rc)) -def on_disconnect(client: mqtt.Client, userdata, rc): - ''' - Función que se ejecuta cuando se desconecta del broker. - Intenta reconectar al bróker. - ''' - print("Desconectado con mensaje:" + str(mqtt.connack_string(rc))) - print("Reconectando...") - client.reconnect() +def on_disconnect(client, userdata, rc): + print("⚠️ Desconectado:", mqtt.connack_string(rc)) + print("Intentando reconectar...") -print("Iniciando cliente MQTT...", settings.MQTT_HOST, settings.MQTT_PORT) +print("🚀 Iniciando cliente MQTT...", settings.MQTT_HOST, settings.MQTT_PORT) + try: - client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, settings.MQTT_USER) + # Cliente configurado para WebSockets (puerto 8082) + client = mqtt.Client( + client_id=settings.MQTT_USER, + transport="websockets" + ) + + # Ruta WebSocket típica (ajustar si tu broker usa otra) + client.ws_set_options(path="/mqtt") + client.on_connect = on_connect client.on_message = on_message client.on_disconnect = on_disconnect if settings.MQTT_USE_TLS: - client.tls_set(ca_certs=settings.CA_CRT_PATH, - tls_version=ssl.PROTOCOL_TLSv1_2, cert_reqs=ssl.CERT_NONE) + client.tls_set( + ca_certs=settings.CA_CRT_PATH, + tls_version=ssl.PROTOCOL_TLSv1_2, + cert_reqs=ssl.CERT_NONE + ) client.username_pw_set(settings.MQTT_USER, settings.MQTT_PASSWORD) - client.connect(settings.MQTT_HOST, settings.MQTT_PORT) + + client.connect(settings.MQTT_HOST, settings.MQTT_PORT, keepalive=60) + + # Loop bloqueante (mantiene conexión viva) + client.loop_forever() except Exception as e: - print('Ocurrió un error al conectar con el bróker MQTT:', e) + print("❌ Error conectando al broker MQTT:", e) \ No newline at end of file From e572688378b91adf6f4d95a3b82e2a67f0fffba6 Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Wed, 25 Feb 2026 17:56:29 -0500 Subject: [PATCH 06/31] Ajustar mqtt a / --- receiver/mqtt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/receiver/mqtt.py b/receiver/mqtt.py index 56972ee4..3855e643 100644 --- a/receiver/mqtt.py +++ b/receiver/mqtt.py @@ -63,7 +63,7 @@ def on_disconnect(client, userdata, rc): ) # Ruta WebSocket típica (ajustar si tu broker usa otra) - client.ws_set_options(path="/mqtt") + client.ws_set_options(path="/") client.on_connect = on_connect client.on_message = on_message From 4689f8041fe59a597e5783406bf30a18c77cb9dd Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Wed, 25 Feb 2026 18:16:21 -0500 Subject: [PATCH 07/31] Ajustar mqtt a / --- receiver/mqtt.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/receiver/mqtt.py b/receiver/mqtt.py index 3855e643..68a73c96 100644 --- a/receiver/mqtt.py +++ b/receiver/mqtt.py @@ -56,14 +56,8 @@ def on_disconnect(client, userdata, rc): print("🚀 Iniciando cliente MQTT...", settings.MQTT_HOST, settings.MQTT_PORT) try: - # Cliente configurado para WebSockets (puerto 8082) - client = mqtt.Client( - client_id=settings.MQTT_USER, - transport="websockets" - ) - - # Ruta WebSocket típica (ajustar si tu broker usa otra) - client.ws_set_options(path="/") + # ✅ MQTT TCP normal (Mosquitto en 8082 sin websockets) + client = mqtt.Client(client_id=settings.MQTT_USER) client.on_connect = on_connect client.on_message = on_message @@ -77,7 +71,6 @@ def on_disconnect(client, userdata, rc): ) client.username_pw_set(settings.MQTT_USER, settings.MQTT_PASSWORD) - client.connect(settings.MQTT_HOST, settings.MQTT_PORT, keepalive=60) # Loop bloqueante (mantiene conexión viva) From 8dda8c966943b098c7675825ed4effbb98918287 Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Wed, 25 Feb 2026 18:46:09 -0500 Subject: [PATCH 08/31] Ajuste variables --- IOTMonitoringServer/settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/IOTMonitoringServer/settings.py b/IOTMonitoringServer/settings.py index 0274cd5a..e522f0c7 100644 --- a/IOTMonitoringServer/settings.py +++ b/IOTMonitoringServer/settings.py @@ -27,7 +27,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ["localhost", "52.55.153.138"] +ALLOWED_HOSTS = ["localhost", "54.204.218.42"] # Application definition @@ -96,7 +96,7 @@ "NAME": "iot_data", # Nombre de la base de datos "USER": "dbadmin", # Nombre de usuario "PASSWORD": "uniandesIOT1234*", # Contraseña - "HOST": "3.95.244.217", # Dirección IP de la base de datos + "HOST": "44.204.192.64", # Dirección IP de la base de datos "PORT": "", # Puerto de la base de datos } } @@ -156,7 +156,7 @@ CRISPY_TEMPLATE_PACK = 'bootstrap4' # Dirección del bróker MQTT -MQTT_HOST = "54.211.59.63" +MQTT_HOST = "3.83.134.224" # Puerto del bróker MQTT MQTT_PORT = 8082 From 2055fa50409e9341838864a0f0659365aeb6d2f2 Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Wed, 25 Feb 2026 23:18:11 -0500 Subject: [PATCH 09/31] Ajuste variables --- IOTMonitoringServer/settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/IOTMonitoringServer/settings.py b/IOTMonitoringServer/settings.py index e522f0c7..bfc5e392 100644 --- a/IOTMonitoringServer/settings.py +++ b/IOTMonitoringServer/settings.py @@ -27,7 +27,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ["localhost", "54.204.218.42"] +ALLOWED_HOSTS = ["localhost", "192.168.0.5"] # Application definition @@ -96,7 +96,7 @@ "NAME": "iot_data", # Nombre de la base de datos "USER": "dbadmin", # Nombre de usuario "PASSWORD": "uniandesIOT1234*", # Contraseña - "HOST": "44.204.192.64", # Dirección IP de la base de datos + "HOST": "", # Dirección IP de la base de datos "PORT": "", # Puerto de la base de datos } } @@ -156,7 +156,7 @@ CRISPY_TEMPLATE_PACK = 'bootstrap4' # Dirección del bróker MQTT -MQTT_HOST = "3.83.134.224" +MQTT_HOST = "13.221.87.38" # Puerto del bróker MQTT MQTT_PORT = 8082 From f6c596167321a0a95567065157b92deb818f2621 Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Wed, 25 Feb 2026 23:43:08 -0500 Subject: [PATCH 10/31] Ajuste variables --- IOTMonitoringServer/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IOTMonitoringServer/settings.py b/IOTMonitoringServer/settings.py index bfc5e392..58d2aa2f 100644 --- a/IOTMonitoringServer/settings.py +++ b/IOTMonitoringServer/settings.py @@ -96,7 +96,7 @@ "NAME": "iot_data", # Nombre de la base de datos "USER": "dbadmin", # Nombre de usuario "PASSWORD": "uniandesIOT1234*", # Contraseña - "HOST": "", # Dirección IP de la base de datos + "HOST": "18.212.67.113", # Dirección IP de la base de datos "PORT": "", # Puerto de la base de datos } } From ea1e26a90129333ab2b77fe5296d278634910480 Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Wed, 25 Feb 2026 23:51:44 -0500 Subject: [PATCH 11/31] Ajuste variables --- IOTMonitoringServer/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IOTMonitoringServer/settings.py b/IOTMonitoringServer/settings.py index 58d2aa2f..c4c82951 100644 --- a/IOTMonitoringServer/settings.py +++ b/IOTMonitoringServer/settings.py @@ -27,7 +27,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ["localhost", "192.168.0.5"] +ALLOWED_HOSTS = ["localhost", "13.219.77.134"] # Application definition From 57d892a4dc4a1d43337fae94f4698336c2543c09 Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Fri, 27 Feb 2026 11:34:13 -0500 Subject: [PATCH 12/31] =?UTF-8?q?Cambio=20en=20c=C3=B3digo=20para=20el=20r?= =?UTF-8?q?eto=20S6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- IOTMonitoringServer/settings.py | 6 ++--- control/monitor.py | 41 +++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/IOTMonitoringServer/settings.py b/IOTMonitoringServer/settings.py index c4c82951..ccf4c8ce 100644 --- a/IOTMonitoringServer/settings.py +++ b/IOTMonitoringServer/settings.py @@ -27,7 +27,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ["localhost", "13.219.77.134"] +ALLOWED_HOSTS = ["localhost", "184.72.153.29"] # Application definition @@ -96,7 +96,7 @@ "NAME": "iot_data", # Nombre de la base de datos "USER": "dbadmin", # Nombre de usuario "PASSWORD": "uniandesIOT1234*", # Contraseña - "HOST": "18.212.67.113", # Dirección IP de la base de datos + "HOST": "54.167.188.146", # Dirección IP de la base de datos "PORT": "", # Puerto de la base de datos } } @@ -156,7 +156,7 @@ CRISPY_TEMPLATE_PACK = 'bootstrap4' # Dirección del bróker MQTT -MQTT_HOST = "13.221.87.38" +MQTT_HOST = "3.94.203.4" # Puerto del bróker MQTT MQTT_PORT = 8082 diff --git a/control/monitor.py b/control/monitor.py index 43d7af0c..efe69f7e 100644 --- a/control/monitor.py +++ b/control/monitor.py @@ -59,6 +59,46 @@ def analyze_data(): print(alerts, "alertas enviadas") +# Umbral de temperatura (ºC) para activar el evento LED +LED_EVENT_TEMP_THRESHOLD = 28.0 + + +def evaluate_led_event(): + """ + Nuevo evento: si temperatura_promedio (última hora, por estación) > umbral, + se envía LED_ON al dispositivo. La temperatura_promedio se obtiene por consulta a la BD. + Acción: el dispositivo parpadea el LED y muestra "Evento: LED activado" en la OLED. + """ + print("Evaluando evento LED (temperatura_promedio > {} °C)...".format(LED_EVENT_TEMP_THRESHOLD)) + + # Consulta a la BD: promedio de temperatura por estación en la última hora + data = Data.objects.filter( + base_time__gte=datetime.now() - timedelta(hours=1), + measurement__name='temperatura' + ) + aggregation = data.values( + 'station__user__username', + 'station__location__city__name', + 'station__location__state__name', + 'station__location__country__name' + ).annotate(temperatura_promedio=Avg('avg_value')) + + sent = 0 + for item in aggregation: + temp_prom = item.get('temperatura_promedio') + if temp_prom is not None and temp_prom > LED_EVENT_TEMP_THRESHOLD: + country = item['station__location__country__name'] + state = item['station__location__state__name'] + city = item['station__location__city__name'] + user = item['station__user__username'] + topic = '{}/{}/{}/{}/in'.format(country, state, city, user) + client.publish(topic, 'LED_ON') + print(datetime.now(), "LED_ON enviado a", topic, "(temperatura_promedio = {:.1f} °C)".format(temp_prom)) + sent += 1 + + print(sent, "comandos LED_ON enviados") + + def on_connect(client, userdata, flags, rc): ''' Función que se ejecuta cuando se conecta al bróker. @@ -106,6 +146,7 @@ def start_cron(): ''' print("Iniciando cron...") schedule.every(5).minutes.do(analyze_data) + schedule.every(5).minutes.do(evaluate_led_event) print("Servicio de control iniciado") while 1: schedule.run_pending() From 958d1b7dd99f674785ea83b3ca564902d3fd0197 Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Fri, 27 Feb 2026 17:31:01 -0500 Subject: [PATCH 13/31] Ajuste temperatura --- IOTMonitoringServer/settings.py | 6 +++--- control/monitor.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/IOTMonitoringServer/settings.py b/IOTMonitoringServer/settings.py index ccf4c8ce..fa3338c2 100644 --- a/IOTMonitoringServer/settings.py +++ b/IOTMonitoringServer/settings.py @@ -27,7 +27,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ["localhost", "184.72.153.29"] +ALLOWED_HOSTS = ["localhost", "54.173.214.232"] # Application definition @@ -96,7 +96,7 @@ "NAME": "iot_data", # Nombre de la base de datos "USER": "dbadmin", # Nombre de usuario "PASSWORD": "uniandesIOT1234*", # Contraseña - "HOST": "54.167.188.146", # Dirección IP de la base de datos + "HOST": "54.237.90.95", # Dirección IP de la base de datos "PORT": "", # Puerto de la base de datos } } @@ -156,7 +156,7 @@ CRISPY_TEMPLATE_PACK = 'bootstrap4' # Dirección del bróker MQTT -MQTT_HOST = "3.94.203.4" +MQTT_HOST = "3.82.193.169" # Puerto del bróker MQTT MQTT_PORT = 8082 diff --git a/control/monitor.py b/control/monitor.py index efe69f7e..794969e2 100644 --- a/control/monitor.py +++ b/control/monitor.py @@ -60,7 +60,7 @@ def analyze_data(): # Umbral de temperatura (ºC) para activar el evento LED -LED_EVENT_TEMP_THRESHOLD = 28.0 +LED_EVENT_TEMP_THRESHOLD = 22.0 def evaluate_led_event(): From d0fd4d850b40ffc851e8bcd7ab27b994766afa61 Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Fri, 27 Feb 2026 18:01:35 -0500 Subject: [PATCH 14/31] chequear el evento del led --- .../management/commands/check_led_event.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 control/management/commands/check_led_event.py diff --git a/control/management/commands/check_led_event.py b/control/management/commands/check_led_event.py new file mode 100644 index 00000000..a29f36fe --- /dev/null +++ b/control/management/commands/check_led_event.py @@ -0,0 +1,65 @@ +""" +Comando para verificar si hay datos de temperatura y si se dispararía el evento LED. +Uso: python manage.py check_led_event +""" +from django.core.management.base import BaseCommand +from django.db.models import Avg +from datetime import timedelta, datetime +from receiver.models import Data + +from control.monitor import LED_EVENT_TEMP_THRESHOLD, evaluate_led_event + + +class Command(BaseCommand): + help = 'Verifica datos de temperatura y opcionalmente envía LED_ON (--send)' + + def add_arguments(self, parser): + parser.add_argument( + '--send', + action='store_true', + help='Ejecutar evaluate_led_event y enviar LED_ON si aplica', + ) + + def handle(self, *args, **options): + data = Data.objects.filter( + base_time__gte=datetime.now() - timedelta(hours=1), + measurement__name='temperatura' + ) + aggregation = data.values( + 'station__user__username', + 'station__location__city__name', + 'station__location__state__name', + 'station__location__country__name' + ).annotate(temperatura_promedio=Avg('avg_value')) + + if not aggregation: + self.stdout.write( + self.style.WARNING( + 'No hay datos de temperatura en la última hora. ' + '¿El receptor (start_mqtt) está corriendo y el dispositivo publicando?' + ) + ) + return + + self.stdout.write('Umbral para LED: {} °C\n'.format(LED_EVENT_TEMP_THRESHOLD)) + for item in aggregation: + temp = item.get('temperatura_promedio') + topic = '{}/{}/{}/{}/in'.format( + item['station__location__country__name'], + item['station__location__state__name'], + item['station__location__city__name'], + item['station__user__username'], + ) + dispara = temp is not None and temp > LED_EVENT_TEMP_THRESHOLD + self.stdout.write( + ' {} | temp_prom = {:.1f} °C | ¿Dispara LED? {}'.format( + topic, temp or 0, 'Sí' if dispara else 'No' + ) + ) + + if options['send']: + self.stdout.write('') + from control import monitor + monitor.setup_mqtt() + evaluate_led_event() + self.stdout.write(self.style.SUCCESS('Listo. Si había temp > umbral, se envió LED_ON. Revisa el NodeMCU (Serial/OLED/LED).')) From 1c58b0099ed42c70cf730377977fdce4a6c2ffe3 Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Fri, 27 Feb 2026 18:12:37 -0500 Subject: [PATCH 15/31] Corregir Timezone --- receiver/mqtt.py | 4 ++-- receiver/utils.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/receiver/mqtt.py b/receiver/mqtt.py index 68a73c96..db3f948b 100644 --- a/receiver/mqtt.py +++ b/receiver/mqtt.py @@ -1,6 +1,6 @@ -from datetime import datetime from . import utils import json +from django.utils import timezone import ssl import paho.mqtt.client as mqtt from django.conf import settings @@ -11,7 +11,7 @@ def on_message(client: mqtt.Client, userdata, message: mqtt.MQTTMessage): Función que se ejecuta cada que llega un mensaje al tópico. """ try: - time = datetime.now() + time = timezone.now() payload = message.payload.decode("utf-8") print("Payload recibido:", payload) diff --git a/receiver/utils.py b/receiver/utils.py index 04cbd71f..81dbdada 100644 --- a/receiver/utils.py +++ b/receiver/utils.py @@ -110,13 +110,15 @@ def create_data( value: float, station: Station, measure: Measurement, - time: datetime = datetime.now(), + time: datetime = None, ): ''' Crea un nuevo dato con valor {value}, estación {station} y variable {measure}. Hace las operaciones necesarias para insertarlo en la base de datos con el patrón Blob. Calcula promedio, mínimo y máximo de los datos anteriores. ''' + if time is None: + time = timezone.now() base_time = datetime(time.year, time.month, time.day, time.hour, tzinfo=time.tzinfo) From 6ca85dcd428ba3d71230546d14b94e894cc3f26b Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Fri, 27 Feb 2026 18:36:21 -0500 Subject: [PATCH 16/31] Ajuste a 2 minutos --- control/monitor.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/control/monitor.py b/control/monitor.py index 794969e2..9e6a88ed 100644 --- a/control/monitor.py +++ b/control/monitor.py @@ -2,6 +2,7 @@ import ssl from django.db.models import Avg from datetime import timedelta, datetime +from django.utils import timezone from receiver.models import Data, Measurement import paho.mqtt.client as mqtt import schedule @@ -19,7 +20,7 @@ def analyze_data(): print("Calculando alertas...") data = Data.objects.filter( - base_time__gte=datetime.now() - timedelta(hours=1)) + base_time__gte=timezone.now() - timedelta(hours=1)) aggregation = data.annotate(check_value=Avg('avg_value')) \ .select_related('station', 'measurement') \ .select_related('station__user', 'station__location') \ @@ -51,7 +52,7 @@ def analyze_data(): if alert: message = "ALERT {} {} {}".format(variable, min_value, max_value) topic = '{}/{}/{}/{}/in'.format(country, state, city, user) - print(datetime.now(), "Sending alert to {} {}".format(topic, variable)) + print(timezone.now(), "Sending alert to {} {}".format(topic, variable)) client.publish(topic, message) alerts += 1 @@ -73,7 +74,7 @@ def evaluate_led_event(): # Consulta a la BD: promedio de temperatura por estación en la última hora data = Data.objects.filter( - base_time__gte=datetime.now() - timedelta(hours=1), + base_time__gte=timezone.now() - timedelta(hours=1), measurement__name='temperatura' ) aggregation = data.values( @@ -93,7 +94,7 @@ def evaluate_led_event(): user = item['station__user__username'] topic = '{}/{}/{}/{}/in'.format(country, state, city, user) client.publish(topic, 'LED_ON') - print(datetime.now(), "LED_ON enviado a", topic, "(temperatura_promedio = {:.1f} °C)".format(temp_prom)) + print(timezone.now(), "LED_ON enviado a", topic, "(temperatura_promedio = {:.1f} °C)".format(temp_prom)) sent += 1 print(sent, "comandos LED_ON enviados") @@ -142,12 +143,12 @@ def setup_mqtt(): def start_cron(): ''' - Inicia el cron que se encarga de ejecutar la función analyze_data cada 5 minutos. + Inicia el cron: analyze_data y evaluate_led_event cada 2 minutos. ''' print("Iniciando cron...") - schedule.every(5).minutes.do(analyze_data) - schedule.every(5).minutes.do(evaluate_led_event) - print("Servicio de control iniciado") + schedule.every(2).minutes.do(analyze_data) + schedule.every(2).minutes.do(evaluate_led_event) + print("Servicio de control iniciado (eventos cada 2 min)") while 1: schedule.run_pending() time.sleep(1) From ef70c0c44bd4d61e306d1a86cf883866e19879ea Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Fri, 27 Feb 2026 18:44:36 -0500 Subject: [PATCH 17/31] =?UTF-8?q?Conexi=C3=B3n=20a=20NodeMCU=20-=20Broker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- control/monitor.py | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/control/monitor.py b/control/monitor.py index 9e6a88ed..d55c7c6e 100644 --- a/control/monitor.py +++ b/control/monitor.py @@ -52,8 +52,17 @@ def analyze_data(): if alert: message = "ALERT {} {} {}".format(variable, min_value, max_value) topic = '{}/{}/{}/{}/in'.format(country, state, city, user) - print(timezone.now(), "Sending alert to {} {}".format(topic, variable)) - client.publish(topic, message) + if not client.is_connected(): + try: + client.reconnect() + time.sleep(0.5) + except Exception: + pass + if client.is_connected(): + client.publish(topic, message) + print(timezone.now(), "Sending alert to {} {}".format(topic, variable)) + else: + print(timezone.now(), "NO enviado (desconectado): alert a", topic) alerts += 1 print(len(aggregation), "dispositivos revisados") @@ -93,9 +102,18 @@ def evaluate_led_event(): city = item['station__location__city__name'] user = item['station__user__username'] topic = '{}/{}/{}/{}/in'.format(country, state, city, user) - client.publish(topic, 'LED_ON') - print(timezone.now(), "LED_ON enviado a", topic, "(temperatura_promedio = {:.1f} °C)".format(temp_prom)) - sent += 1 + if not client.is_connected(): + try: + client.reconnect() + time.sleep(0.5) + except Exception: + pass + if client.is_connected(): + client.publish(topic, 'LED_ON') + print(timezone.now(), "LED_ON enviado a", topic, "(temperatura_promedio = {:.1f} °C)".format(temp_prom)) + sent += 1 + else: + print(timezone.now(), "NO enviado LED_ON (desconectado):", topic) print(sent, "comandos LED_ON enviados") @@ -150,5 +168,12 @@ def start_cron(): schedule.every(2).minutes.do(evaluate_led_event) print("Servicio de control iniciado (eventos cada 2 min)") while 1: + if client.is_connected(): + client.loop(timeout=0.1) + else: + try: + client.reconnect() + except Exception: + pass schedule.run_pending() time.sleep(1) From 36f59c2bff5d47470be60ff84495aeef005b7d73 Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Fri, 27 Feb 2026 18:56:44 -0500 Subject: [PATCH 18/31] =?UTF-8?q?Conexi=C3=B3n=20de=20IOT=20Alert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- control/monitor.py | 67 ++++++++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/control/monitor.py b/control/monitor.py index d55c7c6e..b81cd8b5 100644 --- a/control/monitor.py +++ b/control/monitor.py @@ -9,7 +9,9 @@ import time from django.conf import settings -client = mqtt.Client(settings.MQTT_USER_PUB) +# Client ID único para no chocar con el receptor (admin/admin2 en el broker) +client = mqtt.Client(client_id=settings.MQTT_USER_PUB + "_control") +mqtt_connected = False def analyze_data(): @@ -52,13 +54,13 @@ def analyze_data(): if alert: message = "ALERT {} {} {}".format(variable, min_value, max_value) topic = '{}/{}/{}/{}/in'.format(country, state, city, user) - if not client.is_connected(): + if not mqtt_connected: try: client.reconnect() - time.sleep(0.5) - except Exception: - pass - if client.is_connected(): + time.sleep(1) + except Exception as e: + print("Reconnect falló:", e) + if mqtt_connected: client.publish(topic, message) print(timezone.now(), "Sending alert to {} {}".format(topic, variable)) else: @@ -102,13 +104,13 @@ def evaluate_led_event(): city = item['station__location__city__name'] user = item['station__user__username'] topic = '{}/{}/{}/{}/in'.format(country, state, city, user) - if not client.is_connected(): + if not mqtt_connected: try: client.reconnect() - time.sleep(0.5) - except Exception: - pass - if client.is_connected(): + time.sleep(1) + except Exception as e: + print("Reconnect falló:", e) + if mqtt_connected: client.publish(topic, 'LED_ON') print(timezone.now(), "LED_ON enviado a", topic, "(temperatura_promedio = {:.1f} °C)".format(temp_prom)) sent += 1 @@ -122,28 +124,34 @@ def on_connect(client, userdata, flags, rc): ''' Función que se ejecuta cuando se conecta al bróker. ''' - print("Conectando al broker MQTT...", mqtt.connack_string(rc)) + global mqtt_connected + if rc == 0: + mqtt_connected = True + print("Conectado al broker MQTT correctamente.") + else: + mqtt_connected = False + print("Error al conectar al broker MQTT:", mqtt.connack_string(rc)) def on_disconnect(client: mqtt.Client, userdata, rc): ''' Función que se ejecuta cuando se desconecta del broker. - Intenta reconectar al bróker. ''' - print("Desconectado con mensaje:" + str(mqtt.connack_string(rc))) - print("Reconectando...") - client.reconnect() + global mqtt_connected + mqtt_connected = False + print("Desconectado del broker:", mqtt.connack_string(rc)) def setup_mqtt(): ''' - Configura el cliente MQTT para conectarse al broker. + Configura el cliente MQTT y se conecta al broker. Usa loop_start() para + mantener la conexión en un hilo en segundo plano. ''' - + global client, mqtt_connected + mqtt_connected = False print("Iniciando cliente MQTT...", settings.MQTT_HOST, settings.MQTT_PORT) - global client try: - client = mqtt.Client(settings.MQTT_USER_PUB) + client = mqtt.Client(client_id=settings.MQTT_USER_PUB + "_control") client.on_connect = on_connect client.on_disconnect = on_disconnect @@ -153,10 +161,18 @@ def setup_mqtt(): client.username_pw_set(settings.MQTT_USER_PUB, settings.MQTT_PASSWORD_PUB) - client.connect(settings.MQTT_HOST, settings.MQTT_PORT) - + client.connect(settings.MQTT_HOST, settings.MQTT_PORT, keepalive=60) + # Mantener la conexión en un hilo (importante: sin esto la conexión se pierde) + client.loop_start() + # Dar tiempo a que on_connect se ejecute + for _ in range(20): + if mqtt_connected: + break + time.sleep(0.5) + if not mqtt_connected: + print("Aviso: conexión MQTT aún no confirmada. ¿El broker acepta usuario", settings.MQTT_USER_PUB, "? ¿La EC2 puede alcanzar", settings.MQTT_HOST, ":", settings.MQTT_PORT, "?") except Exception as e: - print('Ocurrió un error al conectar con el bróker MQTT:', e) + print('Error al conectar con el bróker MQTT:', e) def start_cron(): @@ -168,11 +184,10 @@ def start_cron(): schedule.every(2).minutes.do(evaluate_led_event) print("Servicio de control iniciado (eventos cada 2 min)") while 1: - if client.is_connected(): - client.loop(timeout=0.1) - else: + if not mqtt_connected: try: client.reconnect() + time.sleep(1) except Exception: pass schedule.run_pending() From 9fa1267a5a8eb8176ab7c9ba6d4e2fc51868a630 Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Fri, 27 Feb 2026 19:58:56 -0500 Subject: [PATCH 19/31] Prueba de LED --- .../management/commands/check_led_event.py | 93 ++++++++++++++----- 1 file changed, 72 insertions(+), 21 deletions(-) diff --git a/control/management/commands/check_led_event.py b/control/management/commands/check_led_event.py index a29f36fe..326d819b 100644 --- a/control/management/commands/check_led_event.py +++ b/control/management/commands/check_led_event.py @@ -1,28 +1,53 @@ """ Comando para verificar si hay datos de temperatura y si se dispararía el evento LED. Uso: python manage.py check_led_event + python manage.py check_led_event --send # envía LED_ON si temp > umbral + python manage.py check_led_event --send --force # envía LED_ON sin importar la temperatura (prueba) """ from django.core.management.base import BaseCommand from django.db.models import Avg from datetime import timedelta, datetime -from receiver.models import Data +from django.utils import timezone +from receiver.models import Data, Station from control.monitor import LED_EVENT_TEMP_THRESHOLD, evaluate_led_event +def get_topics_from_db(): + """Obtiene los tópicos in de todas las estaciones (para enviar LED_ON).""" + stations = Station.objects.select_related( + 'user', 'location', 'location__city', 'location__state', 'location__country' + ).filter(active=True) + topics = [] + for s in stations: + topic = '{}/{}/{}/{}/in'.format( + s.location.country.name, + s.location.state.name, + s.location.city.name, + s.user.username, + ) + topics.append(topic) + return topics + + class Command(BaseCommand): - help = 'Verifica datos de temperatura y opcionalmente envía LED_ON (--send)' + help = 'Verifica datos de temperatura y opcionalmente envía LED_ON (--send). Use --send --force para probar sin esperar 22°C.' def add_arguments(self, parser): parser.add_argument( '--send', action='store_true', - help='Ejecutar evaluate_led_event y enviar LED_ON si aplica', + help='Enviar LED_ON (si temp > umbral o si usa --force)', + ) + parser.add_argument( + '--force', + action='store_true', + help='Enviar LED_ON a todas las estaciones sin comprobar temperatura (solo para prueba)', ) def handle(self, *args, **options): data = Data.objects.filter( - base_time__gte=datetime.now() - timedelta(hours=1), + base_time__gte=timezone.now() - timedelta(hours=1), measurement__name='temperatura' ) aggregation = data.values( @@ -32,34 +57,60 @@ def handle(self, *args, **options): 'station__location__country__name' ).annotate(temperatura_promedio=Avg('avg_value')) - if not aggregation: + if not aggregation and not options['force']: self.stdout.write( self.style.WARNING( 'No hay datos de temperatura en la última hora. ' '¿El receptor (start_mqtt) está corriendo y el dispositivo publicando?' ) ) + if options['send'] and options['force']: + topics = get_topics_from_db() + if not topics: + self.stdout.write(self.style.WARNING('No hay estaciones en la BD. Publique algo desde el dispositivo primero.')) + return + self.stdout.write('Enviando LED_ON (--force) a: ' + ', '.join(topics)) + from control import monitor + monitor.setup_mqtt() + for topic in topics: + monitor.client.publish(topic, 'LED_ON') + self.stdout.write(self.style.SUCCESS('LED_ON enviado. Revisa el NodeMCU.')) return - self.stdout.write('Umbral para LED: {} °C\n'.format(LED_EVENT_TEMP_THRESHOLD)) - for item in aggregation: - temp = item.get('temperatura_promedio') - topic = '{}/{}/{}/{}/in'.format( - item['station__location__country__name'], - item['station__location__state__name'], - item['station__location__city__name'], - item['station__user__username'], - ) - dispara = temp is not None and temp > LED_EVENT_TEMP_THRESHOLD - self.stdout.write( - ' {} | temp_prom = {:.1f} °C | ¿Dispara LED? {}'.format( - topic, temp or 0, 'Sí' if dispara else 'No' + if aggregation: + self.stdout.write('Umbral para LED: {} °C\n'.format(LED_EVENT_TEMP_THRESHOLD)) + for item in aggregation: + temp = item.get('temperatura_promedio') + topic = '{}/{}/{}/{}/in'.format( + item['station__location__country__name'], + item['station__location__state__name'], + item['station__location__city__name'], + item['station__user__username'], + ) + dispara = temp is not None and temp > LED_EVENT_TEMP_THRESHOLD + self.stdout.write( + ' {} | temp_prom = {:.1f} °C | ¿Dispara LED? {}'.format( + topic, temp or 0, 'Sí' if dispara else 'No' + ) ) - ) if options['send']: self.stdout.write('') from control import monitor monitor.setup_mqtt() - evaluate_led_event() - self.stdout.write(self.style.SUCCESS('Listo. Si había temp > umbral, se envió LED_ON. Revisa el NodeMCU (Serial/OLED/LED).')) + if options['force']: + topics = get_topics_from_db() if not aggregation else [ + '{}/{}/{}/{}/in'.format( + item['station__location__country__name'], + item['station__location__state__name'], + item['station__location__city__name'], + item['station__user__username'], + ) + for item in aggregation + ] + for topic in topics: + monitor.client.publish(topic, 'LED_ON') + self.stdout.write(self.style.SUCCESS('LED_ON enviado (--force) a {} tópico(s). Revisa el NodeMCU.'.format(len(topics)))) + else: + evaluate_led_event() + self.stdout.write(self.style.SUCCESS('Listo. Si había temp > umbral, se envió LED_ON. Revisa el NodeMCU (Serial/OLED/LED).')) From 3bb110b5248bb52879ca35922950e806644b656d Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Fri, 27 Feb 2026 20:04:09 -0500 Subject: [PATCH 20/31] Prueba led --- .../management/commands/check_led_event.py | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/control/management/commands/check_led_event.py b/control/management/commands/check_led_event.py index 326d819b..a3cecc84 100644 --- a/control/management/commands/check_led_event.py +++ b/control/management/commands/check_led_event.py @@ -3,7 +3,12 @@ Uso: python manage.py check_led_event python manage.py check_led_event --send # envía LED_ON si temp > umbral python manage.py check_led_event --send --force # envía LED_ON sin importar la temperatura (prueba) + +El comando usa un client_id distinto al de start_control para no desconectar el broker. """ +import time +import paho.mqtt.client as mqtt +from django.conf import settings from django.core.management.base import BaseCommand from django.db.models import Avg from datetime import timedelta, datetime @@ -13,6 +18,24 @@ from control.monitor import LED_EVENT_TEMP_THRESHOLD, evaluate_led_event +def send_led_on_to_topics(topics): + """ + Envía LED_ON a los tópicos usando un cliente MQTT propio (client_id distinto a start_control). + Así no se desconecta el proceso start_control que ya está corriendo. + """ + client_id = settings.MQTT_USER_PUB + "_check_led" + c = mqtt.Client(client_id=client_id) + c.username_pw_set(settings.MQTT_USER_PUB, settings.MQTT_PASSWORD_PUB) + c.connect(settings.MQTT_HOST, settings.MQTT_PORT, keepalive=60) + c.loop_start() + time.sleep(1) # dar tiempo a conectar + for topic in topics: + c.publish(topic, 'LED_ON') + time.sleep(0.5) + c.loop_stop() + c.disconnect() + + def get_topics_from_db(): """Obtiene los tópicos in de todas las estaciones (para enviar LED_ON).""" stations = Station.objects.select_related( @@ -70,10 +93,7 @@ def handle(self, *args, **options): self.stdout.write(self.style.WARNING('No hay estaciones en la BD. Publique algo desde el dispositivo primero.')) return self.stdout.write('Enviando LED_ON (--force) a: ' + ', '.join(topics)) - from control import monitor - monitor.setup_mqtt() - for topic in topics: - monitor.client.publish(topic, 'LED_ON') + send_led_on_to_topics(topics) self.stdout.write(self.style.SUCCESS('LED_ON enviado. Revisa el NodeMCU.')) return @@ -96,8 +116,6 @@ def handle(self, *args, **options): if options['send']: self.stdout.write('') - from control import monitor - monitor.setup_mqtt() if options['force']: topics = get_topics_from_db() if not aggregation else [ '{}/{}/{}/{}/in'.format( @@ -108,9 +126,10 @@ def handle(self, *args, **options): ) for item in aggregation ] - for topic in topics: - monitor.client.publish(topic, 'LED_ON') + send_led_on_to_topics(topics) self.stdout.write(self.style.SUCCESS('LED_ON enviado (--force) a {} tópico(s). Revisa el NodeMCU.'.format(len(topics)))) else: + from control import monitor + monitor.setup_mqtt() evaluate_led_event() self.stdout.write(self.style.SUCCESS('Listo. Si había temp > umbral, se envió LED_ON. Revisa el NodeMCU (Serial/OLED/LED).')) From e8b369426294ba25ac5870fa577bbc5fc30916a8 Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Fri, 27 Feb 2026 20:21:39 -0500 Subject: [PATCH 21/31] Viewer --- IOTMonitoringServer/settings.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/IOTMonitoringServer/settings.py b/IOTMonitoringServer/settings.py index fa3338c2..f418b04d 100644 --- a/IOTMonitoringServer/settings.py +++ b/IOTMonitoringServer/settings.py @@ -27,7 +27,13 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ["localhost", "54.173.214.232"] +ALLOWED_HOSTS = [ + "localhost", + "54.173.214.232", + "ec2-54-173-214-232.compute-1.amazonaws.com", + "ip-10-0-18-78.ec2.internal", + ".compute-1.amazonaws.com", # cualquier EC2 en esa región por hostname +] # Application definition From adcc90818cf5523ce851c32db805366ce0a99eb1 Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Fri, 27 Feb 2026 20:27:00 -0500 Subject: [PATCH 22/31] fix superuser --- viewer/utils.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/viewer/utils.py b/viewer/utils.py index c027d5c7..8d41a74d 100644 --- a/viewer/utils.py +++ b/viewer/utils.py @@ -122,18 +122,26 @@ def get_realtime_context(request): user = User.objects.get(username=userParam) print("CONTEXT: getting user db: ", user) stations = Station.objects.filter(user=user) + # Si el usuario no tiene estaciones pero es superuser, usar la primera estación disponible (ej. ironman) + if not stations.exists() and user.is_superuser: + first_station = Station.objects.filter(active=True).select_related('location').first() + if first_station: + stations = Station.objects.filter(pk=first_station.pk) print("CONTEXT: getting stations db: ", stations) - station = stations[0] if len(stations) > 0 else None + station = stations.first() print("CONTEXT: getting first station: ", station) if station != None: cityParam = station.location.city.name stateParam = station.location.state.name countryParam = station.location.country.name + data_user = station.user.username # datos de quien tenga la estación (ej. ironman) else: return context + else: + data_user = userParam print("CONTEXT: getting last week data and measurements") context["data"], context["measurements"] = get_last_week_data( - userParam, cityParam, stateParam, countryParam + data_user, cityParam, stateParam, countryParam ) print( "CONTEXT: got last week data, now getting city, state, country: ", From ff6d7492f868cf836202151539de3174637b51b7 Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Fri, 27 Feb 2026 20:33:26 -0500 Subject: [PATCH 23/31] Fix views --- viewer/utils.py | 22 ++++++++++++++++++++++ viewer/views.py | 5 ++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/viewer/utils.py b/viewer/utils.py index 8d41a74d..276b10db 100644 --- a/viewer/utils.py +++ b/viewer/utils.py @@ -11,6 +11,28 @@ def get_measurements(): return list(measurements) +def get_data_user_for_location(city, state, country, request_user): + """ + Devuelve el username a usar para get_last_week_data en esa ubicación. + Si el usuario tiene estación ahí, es él; si no pero es superuser, el dueño de cualquier estación ahí. + """ + try: + cityO = City.objects.get(name=city) + stateO = State.objects.get(name=state) + countryO = Country.objects.get(name=country) + location = Location.objects.get(city=cityO, state=stateO, country=countryO) + except Exception: + return request_user.username + station = Station.objects.filter(user=request_user, location=location).first() + if station: + return request_user.username + if request_user.is_superuser: + any_station = Station.objects.filter(location=location, active=True).select_related('user').first() + if any_station: + return any_station.user.username + return request_user.username + + def get_last_week_data(user, city, state, country): result = {} start = datetime.now() diff --git a/viewer/views.py b/viewer/views.py index 33a7fbc2..bc285db2 100644 --- a/viewer/views.py +++ b/viewer/views.py @@ -43,8 +43,11 @@ def realtime_data(request): cityName = body["city"] stateName = body["state"] countryName = body["country"] + data_user = utils.get_data_user_for_location( + cityName, stateName, countryName, request.user + ) data["result"], measurement = utils.get_last_week_data( - userParam, cityName, stateName, countryName + data_user, cityName, stateName, countryName ) else: data["error"] = "Ha ocurrido un error" From b5f631ee48ce3099bb57d9118c7fce528211ee9f Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Fri, 27 Feb 2026 20:39:01 -0500 Subject: [PATCH 24/31] fix location --- .../commands/fix_placeholder_location.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 receiver/management/commands/fix_placeholder_location.py diff --git a/receiver/management/commands/fix_placeholder_location.py b/receiver/management/commands/fix_placeholder_location.py new file mode 100644 index 00000000..1122ca6c --- /dev/null +++ b/receiver/management/commands/fix_placeholder_location.py @@ -0,0 +1,75 @@ +""" +Corrige ubicaciones que tienen los placeholders "ciudad", "estado", "pais" +y los reemplaza por "bogota", "cundinamarca", "colombia" (o los valores que indiques). +Así el mapa histórico y los datos en tiempo real muestran el nombre real. + +Uso: + python manage.py fix_placeholder_location + python manage.py fix_placeholder_location --city bogota --state cundinamarca --country colombia +""" +from django.core.management.base import BaseCommand +from receiver.models import Location, City, State, Country +from receiver.utils import get_coordinates + + +class Command(BaseCommand): + help = 'Reemplaza placeholders ciudad/estado/pais por nombres reales (ej. bogota, cundinamarca, colombia)' + + def add_arguments(self, parser): + parser.add_argument('--city', default='bogota', help='Nombre de ciudad (default: bogota)') + parser.add_argument('--state', default='cundinamarca', help='Nombre de estado/departamento (default: cundinamarca)') + parser.add_argument('--country', default='colombia', help='Nombre de país (default: colombia)') + + def handle(self, *args, **options): + city_name = options['city'] + state_name = options['state'] + country_name = options['country'] + + # Ubicaciones que tienen los placeholders literales + locations = Location.objects.filter( + city__name='ciudad', + state__name='estado', + country__name='pais' + ).select_related('city', 'state', 'country') + + if not locations.exists(): + self.stdout.write(self.style.WARNING('No hay ubicaciones con "ciudad, estado, pais". Nada que corregir.')) + return + + city_o, _ = City.objects.get_or_create(name=city_name, defaults={}) + state_o, _ = State.objects.get_or_create(name=state_name, defaults={}) + country_o, _ = Country.objects.get_or_create(name=country_name, defaults={}) + + updated = 0 + for loc in locations: + loc.city = city_o + loc.state = state_o + loc.country = country_o + # Actualizar coordenadas para el mapa + try: + lat, lng = get_coordinates(city_name, state_name, country_name) + if lat and lng: + loc.lat = lat + loc.lng = lng + except Exception as e: + self.stdout.write(self.style.WARNING('Coordenadas no actualizadas: {}'.format(e))) + loc.save() + updated += 1 + self.stdout.write('Actualizada Location id={} -> {}, {}, {}'.format( + loc.pk, city_name, state_name, country_name)) + + # Opcional: borrar City/State/Country viejos si ya no los usa nadie + old_city = City.objects.filter(name='ciudad').first() + old_state = State.objects.filter(name='estado').first() + old_country = Country.objects.filter(name='pais').first() + if old_city and not Location.objects.filter(city=old_city).exists(): + old_city.delete() + self.stdout.write('Eliminada City "ciudad"') + if old_state and not Location.objects.filter(state=old_state).exists(): + old_state.delete() + self.stdout.write('Eliminado State "estado"') + if old_country and not Location.objects.filter(country=old_country).exists(): + old_country.delete() + self.stdout.write('Eliminado Country "pais"') + + self.stdout.write(self.style.SUCCESS('Listo. {} ubicación(es) corregida(s). Recarga el mapa histórico.'.format(updated))) From d6aa41f7387339ec7c6965a9d6b4480bdb2db1a5 Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Fri, 27 Feb 2026 20:42:21 -0500 Subject: [PATCH 25/31] Fix location 2 --- .../commands/fix_placeholder_location.py | 60 ++++++++++++------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/receiver/management/commands/fix_placeholder_location.py b/receiver/management/commands/fix_placeholder_location.py index 1122ca6c..69a100eb 100644 --- a/receiver/management/commands/fix_placeholder_location.py +++ b/receiver/management/commands/fix_placeholder_location.py @@ -1,7 +1,7 @@ """ -Corrige ubicaciones que tienen los placeholders "ciudad", "estado", "pais" -y los reemplaza por "bogota", "cundinamarca", "colombia" (o los valores que indiques). -Así el mapa histórico y los datos en tiempo real muestran el nombre real. +Corrige ubicaciones que tienen los placeholders "ciudad", "estado", "pais": +reasigna sus estaciones a la Location real (bogota, cundinamarca, colombia) y elimina la placeholder. +Si la Location real no existe, la crea. Uso: python manage.py fix_placeholder_location @@ -13,7 +13,7 @@ class Command(BaseCommand): - help = 'Reemplaza placeholders ciudad/estado/pais por nombres reales (ej. bogota, cundinamarca, colombia)' + help = 'Reasigna estaciones de ciudad/estado/pais a la ubicación real (ej. bogota, cundinamarca, colombia)' def add_arguments(self, parser): parser.add_argument('--city', default='bogota', help='Nombre de ciudad (default: bogota)') @@ -25,14 +25,13 @@ def handle(self, *args, **options): state_name = options['state'] country_name = options['country'] - # Ubicaciones que tienen los placeholders literales - locations = Location.objects.filter( + placeholder_locations = Location.objects.filter( city__name='ciudad', state__name='estado', country__name='pais' ).select_related('city', 'state', 'country') - if not locations.exists(): + if not placeholder_locations.exists(): self.stdout.write(self.style.WARNING('No hay ubicaciones con "ciudad, estado, pais". Nada que corregir.')) return @@ -40,25 +39,44 @@ def handle(self, *args, **options): state_o, _ = State.objects.get_or_create(name=state_name, defaults={}) country_o, _ = Country.objects.get_or_create(name=country_name, defaults={}) - updated = 0 - for loc in locations: - loc.city = city_o - loc.state = state_o - loc.country = country_o - # Actualizar coordenadas para el mapa + # Location real (donde deben quedar las estaciones) + real_location, created = Location.objects.get_or_create( + city=city_o, state=state_o, country=country_o, + defaults={'active': True} + ) + if created: try: lat, lng = get_coordinates(city_name, state_name, country_name) if lat and lng: - loc.lat = lat - loc.lng = lng + real_location.lat = lat + real_location.lng = lng + real_location.save() except Exception as e: self.stdout.write(self.style.WARNING('Coordenadas no actualizadas: {}'.format(e))) - loc.save() - updated += 1 - self.stdout.write('Actualizada Location id={} -> {}, {}, {}'.format( - loc.pk, city_name, state_name, country_name)) + self.stdout.write('Creada Location {}, {}, {}'.format(city_name, state_name, country_name)) - # Opcional: borrar City/State/Country viejos si ya no los usa nadie + # Reasignar todas las estaciones de las placeholder locations a la Location real + from receiver.models import Station, Data + moved = 0 + for loc in placeholder_locations: + stations = Station.objects.filter(location=loc) + for st in stations: + existing = Station.objects.filter(user=st.user, location=real_location).first() + if existing: + # El usuario ya tiene estación en la Location real: mover los Data a esa estación y borrar la duplicada + Data.objects.filter(station=st).update(station=existing) + st.delete() + self.stdout.write('Estación id={} (user={}) fusionada en estación id={}'.format(st.pk, st.user.username, existing.pk)) + else: + st.location = real_location + st.save() + self.stdout.write('Estación id={} (user={}) -> {}, {}, {}'.format( + st.pk, st.user.username, city_name, state_name, country_name)) + moved += 1 + loc.delete() + self.stdout.write('Eliminada Location placeholder id={}'.format(loc.pk)) + + # Borrar City/State/Country placeholder si ya no los usa nadie old_city = City.objects.filter(name='ciudad').first() old_state = State.objects.filter(name='estado').first() old_country = Country.objects.filter(name='pais').first() @@ -72,4 +90,4 @@ def handle(self, *args, **options): old_country.delete() self.stdout.write('Eliminado Country "pais"') - self.stdout.write(self.style.SUCCESS('Listo. {} ubicación(es) corregida(s). Recarga el mapa histórico.'.format(updated))) + self.stdout.write(self.style.SUCCESS('Listo. {} estación(es) reasignada(s) a {}, {}, {}. Recarga el mapa.'.format(moved, city_name, state_name, country_name))) From 6079db1e9c8e90a9f73a8c6055db7edf4323acfb Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Fri, 27 Feb 2026 20:48:00 -0500 Subject: [PATCH 26/31] Coordenadas --- viewer/utils.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/viewer/utils.py b/viewer/utils.py index 276b10db..9ef9bdb1 100644 --- a/viewer/utils.py +++ b/viewer/utils.py @@ -2,6 +2,7 @@ import traceback from django.contrib.auth.models import User from receiver.models import Measurement, Station, Data, Location, City, State, Country +from receiver.utils import get_coordinates from django.db.models import Avg, Max, Min, Sum import dateutil.relativedelta @@ -225,6 +226,8 @@ def get_map_context(request): for location in locations: stations = Station.objects.filter(location=location) + if not selectedMeasure: + continue locationData = Data.objects.filter( station__in=stations, measurement__name=selectedMeasure.name, time__gte=start_ts, time__lte=end_ts, ) @@ -233,15 +236,30 @@ def get_map_context(request): minVal = locationData.aggregate(Min("min_value"))["min_value__min"] maxVal = locationData.aggregate(Max("max_value"))["max_value__max"] avgVal = locationData.aggregate(Avg("avg_value"))["avg_value__avg"] + lat = location.lat + lng = location.lng + if lat is None or lng is None: + try: + lat, lng = get_coordinates( + location.city.name, location.state.name, location.country.name + ) + if lat and lng: + location.lat = lat + location.lng = lng + location.save() + except Exception: + pass + if lat is None or lng is None: + continue data.append( { "name": f"{location.city.name}, {location.state.name}, {location.country.name}", - "lat": location.lat, - "lng": location.lng, + "lat": float(lat), + "lng": float(lng), "population": stations.count(), - "min": minVal if minVal != None else 0, - "max": maxVal if maxVal != None else 0, - "avg": round(avgVal if avgVal != None else 0, 2), + "min": minVal if minVal is not None else 0, + "max": maxVal if maxVal is not None else 0, + "avg": round(float(avgVal) if avgVal is not None else 0, 2), } ) From 6c0fadd0910e7d92b14a64b56566f2f7fe60dd07 Mon Sep 17 00:00:00 2001 From: HectorFranco-MISO Date: Fri, 27 Feb 2026 20:57:34 -0500 Subject: [PATCH 27/31] Ajuste de rema --- viewer/templates/map.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/viewer/templates/map.html b/viewer/templates/map.html index b590684e..2b52eae4 100644 --- a/viewer/templates/map.html +++ b/viewer/templates/map.html @@ -49,7 +49,7 @@ class="float-right my-3 my-md-0" aria-label="Seleccionar variable" aria-placeholder="Variable" - onchange="this.options[this.selectedIndex].value && (window.location = '/rema/' + this.options[this.selectedIndex].value);" + onchange="var v=this.options[this.selectedIndex].value; if(v) { var url='/map/?measure='+encodeURIComponent(v); var params=new URLSearchParams(window.location.search); if(params.get('from')) url+='&from='+params.get('from'); if(params.get('to')) url+='&to='+params.get('to'); window.location=url; }" > {% for measure in measurements %}