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 %}
+
+
+ Última semana (por defecto)
+
+
Descargar datos
@@ -102,50 +106,55 @@
Datos históricos
$('button[name="daterange"]').click();
}
+ function setDownloadUrl(start, end) {
+ var startTime = start ? +start : null;
+ var endTime = end ? +end : null;
+ var url = '/historic/data/';
+ if (startTime != null && endTime != null) {
+ url += '?from=' + startTime + '&to=' + endTime;
+ }
+ $("#downloadAnchor").attr("href", url).attr("data-url", url);
+ }
+
$(function () {
+ setDownloadUrl(null, null);
$('button[name="daterange"]').daterangepicker(
{
opens: "right",
+ startDate: moment().subtract(6, 'days'),
+ endDate: moment()
},
function (start, end, label) {
- let startTime = +start;
- let endTime = +end;
-
$("#daterange-label").text(
start.format("DD/MM/YYYY") + " - " + end.format("DD/MM/YYYY")
);
-
- $("#downloadAnchor")
- .attr("name", `/historical/data?from=${startTime}&to=${endTime}`)
- .removeClass("disabled");
+ setDownloadUrl(start, end);
+ $("#downloadAnchor").removeClass("disabled");
}
);
});
diff --git a/viewer/urls.py b/viewer/urls.py
index f3af8ee4..579671ac 100644
--- a/viewer/urls.py
+++ b/viewer/urls.py
@@ -8,6 +8,7 @@
path('realtime-data/', views.realtime_data, name='realtime_data'),
path('map/', views.map_data, name='map'),
path('historic/', views.download_data, name='historical'),
+ path('historic/data/', views.download_historical_csv, name='historical_data'),
path('users/', views.users, name='users'),
path('users/delete/
', views.delete_user, name='delete_users'),
path("users/register/", views.register_request, name="register"),
diff --git a/viewer/views.py b/viewer/views.py
index bc285db2..36dc5f0f 100644
--- a/viewer/views.py
+++ b/viewer/views.py
@@ -22,6 +22,12 @@
8. Variables (Admin)
'''
from django.http import HttpResponse, HttpResponsePermanentRedirect, HttpResponseRedirect, JsonResponse
+from django.utils import timezone
+from receiver.models import Data
+import dateutil.relativedelta
+from datetime import datetime
+import csv
+import io
@login_required
@@ -67,6 +73,55 @@ def download_data(request):
return render(request, 'historical.html')
+@login_required
+def download_historical_csv(request):
+ """Genera CSV de datos históricos para el rango from/to (GET, timestamps en ms)."""
+ try:
+ from_ts = request.GET.get('from')
+ to_ts = request.GET.get('to')
+ if from_ts and to_ts:
+ start = datetime.fromtimestamp(float(from_ts) / 1000.0)
+ end = datetime.fromtimestamp(float(to_ts) / 1000.0)
+ else:
+ end = timezone.now()
+ start = end - dateutil.relativedelta.relativedelta(weeks=1)
+ except (TypeError, ValueError):
+ end = timezone.now()
+ start = end - dateutil.relativedelta.relativedelta(weeks=1)
+
+ start_ts = int(start.timestamp() * 1000000)
+ end_ts = int(end.timestamp() * 1000000)
+
+ rows = Data.objects.filter(
+ time__gte=start_ts, time__lte=end_ts
+ ).select_related('station', 'station__user', 'station__location', 'station__location__city',
+ 'station__location__state', 'station__location__country', 'measurement').order_by('time')
+
+ buffer = io.StringIO()
+ writer = csv.writer(buffer)
+ writer.writerow(['Usuario', 'Ciudad', 'Estado', 'Pais', 'Fecha', 'Variable', 'Valor'])
+
+ for d in rows:
+ user = d.station.user.username
+ city = d.station.location.city.name
+ state = d.station.location.state.name
+ country = d.station.location.country.name
+ var = d.measurement.name
+ for i, (t, v) in enumerate(zip(d.times or [], d.values or [])):
+ try:
+ base_ts = d.base_time.timestamp() if hasattr(d.base_time, 'timestamp') else 0
+ secs = float(t) if t is not None else 0
+ dt = datetime.fromtimestamp(base_ts + secs)
+ fecha = dt.strftime('%Y-%m-%d %H:%M:%S')
+ except Exception:
+ fecha = str(d.base_time) if d.base_time else ''
+ writer.writerow([user, city, state, country, fecha, var, v])
+
+ response = HttpResponse(buffer.getvalue(), content_type='text/csv; charset=utf-8')
+ response['Content-Disposition'] = 'attachment; filename="datos-historicos-iot.csv"'
+ return response
+
+
@user_passes_test(lambda u: u.is_superuser)
def users(request):
users = User.objects.all().order_by('id')
From e6cae9e108e816f005417de4986b5ddd14e734e4 Mon Sep 17 00:00:00 2001
From: HectorFranco-MISO
Date: Sat, 28 Feb 2026 13:42:32 -0500
Subject: [PATCH 29/31] Despliegue
---
IOTMonitoringServer/settings.py | 8 +-
README.md | 228 ++++++++++++++++++++++++++++++++
2 files changed, 232 insertions(+), 4 deletions(-)
create mode 100644 README.md
diff --git a/IOTMonitoringServer/settings.py b/IOTMonitoringServer/settings.py
index f418b04d..ad43dc16 100644
--- a/IOTMonitoringServer/settings.py
+++ b/IOTMonitoringServer/settings.py
@@ -29,8 +29,8 @@
ALLOWED_HOSTS = [
"localhost",
- "54.173.214.232",
- "ec2-54-173-214-232.compute-1.amazonaws.com",
+ "3.88.22.107",
+ "ec2-3-54-22-107.compute-1.amazonaws.com",
"ip-10-0-18-78.ec2.internal",
".compute-1.amazonaws.com", # cualquier EC2 en esa región por hostname
]
@@ -102,7 +102,7 @@
"NAME": "iot_data", # Nombre de la base de datos
"USER": "dbadmin", # Nombre de usuario
"PASSWORD": "uniandesIOT1234*", # Contraseña
- "HOST": "54.237.90.95", # Dirección IP de la base de datos
+ "HOST": "44.203.85.217", # Dirección IP de la base de datos
"PORT": "", # Puerto de la base de datos
}
}
@@ -162,7 +162,7 @@
CRISPY_TEMPLATE_PACK = 'bootstrap4'
# Dirección del bróker MQTT
-MQTT_HOST = "3.82.193.169"
+MQTT_HOST = "18.207.213.158"
# Puerto del bróker MQTT
MQTT_PORT = 8082
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..9f61babc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,228 @@
+# Reto: Procesamiento de eventos con consulta a BD y actuador
+
+**Repositorio:** https://github.com/HectorFranco-MISO/IOTMonitoringServer
+
+---
+
+## 1. Objetivo del reto
+
+Se modificó el código del tutorial de la capa de aplicación (lógica) para agregar el **procesamiento de un nuevo evento** que cumple:
+
+- **Condición:** con pre-requisito de **consulta a la base de datos** (temperatura promedio por estación en la última hora).
+- **Acción:** ejecutada por un **actuador del dispositivo IoT** (LED en protoboard + mensaje en pantalla OLED).
+
+---
+
+## 2. Descripción del nuevo evento
+
+| Aspecto | Descripción |
+|--------|-------------|
+| **Condición** | Si la **temperatura promedio** (última hora, por estación) es **mayor a 22 °C**. El valor de temperatura promedio se obtiene mediante una consulta a la base de datos. |
+| **Acción** | El servidor envía el comando `LED_ON` por MQTT al dispositivo. El dispositivo **parpadea un LED** conectado en D6 y muestra en la **OLED** el mensaje *"Evento: LED activado"* durante 60 segundos. |
+| **Frecuencia** | La evaluación se ejecuta cada 2 minutos (junto con el resto del servicio de control). |
+
+---
+
+## 3. Modificaciones realizadas
+
+### 3.1 Servidor (capa de aplicación) — `control/monitor.py`
+
+Se agregó un **nuevo evento** independiente del de alertas ya existente:
+
+1. **Umbral y función del evento LED**
+
+La condición usa un umbral de temperatura (en °C) y una función que consulta la BD y publica `LED_ON` cuando se cumple la condición:
+
+```python
+# Umbral de temperatura (ºC) para activar el evento LED
+LED_EVENT_TEMP_THRESHOLD = 22.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.
+ """
+ # ...
+```
+
+2. **Consulta a la base de datos (pre-requisito de la condición)**
+
+La **temperatura promedio** se obtiene con una consulta al modelo `Data`, filtrando por la última hora y por la medición `temperatura`, agrupando por estación y calculando el promedio de `avg_value`:
+
+```python
+ # Consulta a la BD: promedio de temperatura por estación en la última hora
+ data = Data.objects.filter(
+ base_time__gte=timezone.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'))
+```
+
+3. **Evaluación de la condición y envío de la acción**
+
+Se recorre la agregación y, para cada estación cuyo `temperatura_promedio` supere el umbral, se publica `LED_ON` en el tópico MQTT de entrada del dispositivo:
+
+```python
+ for item in aggregation:
+ temp_prom = item.get('temperatura_promedio')
+ if temp_prom is not None and temp_prom > LED_EVENT_TEMP_THRESHOLD:
+ # ... arma topic desde item ...
+ if mqtt_connected:
+ client.publish(topic, 'LED_ON')
+ # ...
+```
+
+4. **Programación en el cron**
+
+La función del nuevo evento se ejecuta periódicamente junto con el análisis de alertas:
+
+```python
+ schedule.every(2).minutes.do(analyze_data)
+ schedule.every(2).minutes.do(evaluate_led_event)
+```
+
+---
+
+### 3.2 Dispositivo (sketch) — `IOTDeviceScript.ino`
+
+1. **Definiciones para el actuador LED y duración del evento**
+
+```cpp
+#define LED_PIN 12 // D6 en NodeMCU (LED externo en protoboard: D6 y GND)
+#define LED_EVENT_DURATION 60 // segundos que parpadea el LED
+#define LED_BLINK_MS 400 // intervalo de parpadeo en ms
+```
+
+2. **Variables de estado del evento LED**
+
+```cpp
+// Evento LED (parpadeo + mensaje en OLED)
+bool ledEventActive = false;
+unsigned long ledEventStartTime = 0;
+unsigned long ledBlinkLastToggle = 0;
+bool ledBlinkState = false;
+```
+
+3. **Recepción del comando MQTT y activación del evento**
+
+En el callback de MQTT, si el mensaje contiene `LED_ON`, se activa el evento (parpadeo + mensaje en OLED):
+
+```cpp
+ if (data.indexOf("LED_ON") >= 0) {
+ Serial.println("*** EVENTO LED DETECTADO: temperatura_promedio > umbral ***");
+ ledEventActive = true;
+ ledEventStartTime = millis();
+ ledBlinkLastToggle = millis();
+ ledBlinkState = false;
+ digitalWrite(LED_PIN, LOW); // apagado al inicio (D6: LOW = off)
+ }
+```
+
+4. **Lógica del parpadeo y mensaje para la OLED**
+
+En cada iteración del `loop`, se actualiza el estado del LED y se elige el mensaje que se envía a la pantalla; mientras el evento está activo se devuelve *"Evento: LED activado"* y se hace parpadear el LED:
+
+```cpp
+// Actualiza parpadeo del LED por evento y devuelve mensaje para OLED
+String updateLedEventAndMessage() {
+ if (!ledEventActive) return checkAlert();
+
+ unsigned long elapsed = millis() - ledEventStartTime;
+ if (elapsed >= (unsigned long)LED_EVENT_DURATION * 1000UL) {
+ ledEventActive = false;
+ digitalWrite(LED_PIN, LOW);
+ return checkAlert();
+ }
+
+ if (millis() - ledBlinkLastToggle >= (unsigned long)LED_BLINK_MS) {
+ ledBlinkLastToggle = millis();
+ ledBlinkState = !ledBlinkState;
+ digitalWrite(LED_PIN, ledBlinkState ? HIGH : LOW); // D6: HIGH = encendido
+ }
+ return "Evento: LED activado";
+}
+```
+
+5. **Uso en `setup` y `loop`**
+
+En `setup` se configura el pin del LED como salida y estado inicial:
+
+```cpp
+ pinMode(LED_PIN, OUTPUT);
+ digitalWrite(LED_PIN, LOW); // LED apagado al inicio (D6)
+```
+
+En `loop` se usa el mensaje devuelto por `updateLedEventAndMessage()` para actualizar la pantalla:
+
+```cpp
+void loop() {
+ checkWiFi();
+ String message = updateLedEventAndMessage();
+ measure();
+ renderScreen(message);
+}
+```
+
+---
+
+## 4. Configuración física (NodeMCU, protoboard y actuadores)
+
+*Aquí puedes pegar fotos y una breve descripción de tu montaje.*
+
+### 4.1 Esquema de conexiones
+
+Incluye una foto o diagrama de tu configuración (NodeMCU, protoboard, LED, OLED, DHT11).
+
+
+
+**Espacio para imagen 1:** Vista general del NodeMCU y la protoboard.
+
+---
+
+### 4.2 Conexiones utilizadas
+
+| Componente | Conexión en NodeMCU |
+|------------|---------------------|
+| DHT11 | GND, 3V3, D4 (datos) |
+| OLED | D1 (SCL), D2 (SDA), 3V3, GND |
+| LED | D6 (ánodo con resistencia ~220Ω) y GND |
+
+**Espacio para imagen 2:** Detalle del LED en la protoboard (patilla larga/corta y resistencia).
+
+---
+
+### 4.3 Evidencia del evento en funcionamiento
+
+**Espacio para imagen 3:** OLED mostrando *"Evento: LED activado"* cuando se dispara el evento.
+
+**Espacio para imagen 4:** LED parpadeando durante el evento (opcional).
+
+---
+
+## 5. Redespliegue y pruebas
+
+- La aplicación del servidor (incluido el servicio de control con `evaluate_led_event`) se redesplegó en la infraestructura de nube.
+- Se verificó que:
+ - La consulta a la BD devuelve la temperatura promedio por estación.
+ - Cuando la temperatura promedio supera 22 °C, se envía `LED_ON` al tópico correcto.
+ - El dispositivo recibe el mensaje, parpadea el LED en D6 y muestra *"Evento: LED activado"* en la OLED durante 60 segundos.
+
+---
+
+## 6. Repositorio
+
+**Enlace al repositorio GitHub (fork del tutorial):**
+*(pega aquí la URL de tu repositorio)*
+
+---
+
+*Documento preparado para el reto de la capa de aplicación (lógica). Las modificaciones descritas corresponden únicamente al nuevo evento con consulta a BD y actuador (LED + OLED).*
From 3ceee55760befc120e37b3e0d80bea9ccaeebf9f Mon Sep 17 00:00:00 2001
From: HectorFranco-MISO
Date: Sat, 28 Feb 2026 14:56:45 -0500
Subject: [PATCH 30/31] commit final
---
IOTDeviceScript/IOTDeviceScript.ino | 410 ++++++++++++++++++++++++++++
README.md | 45 +--
2 files changed, 411 insertions(+), 44 deletions(-)
create mode 100644 IOTDeviceScript/IOTDeviceScript.ino
diff --git a/IOTDeviceScript/IOTDeviceScript.ino b/IOTDeviceScript/IOTDeviceScript.ino
new file mode 100644
index 00000000..8435a61e
--- /dev/null
+++ b/IOTDeviceScript/IOTDeviceScript.ino
@@ -0,0 +1,410 @@
+#include
+#include
+#include
+#include
+#include
+#include
+
+// =====================
+// DEFINICIONES
+// =====================
+
+#define DHTPIN 2
+#define DHTTYPE DHT11
+
+#define MEASURE_INTERVAL 2
+#define ALERT_DURATION 60
+#define LED_PIN 12 // D6 en NodeMCU (LED externo en protoboard: D6 y GND)
+#define LED_EVENT_DURATION 60 // segundos que parpadea el LED
+#define LED_BLINK_MS 400 // intervalo de parpadeo en ms
+
+// =====================
+// DECLARACIONES
+// =====================
+
+// OLED SH1106 128x64 I2C (sin pin reset)
+U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
+
+// Sensor DHT
+DHT dht(DHTPIN, DHTTYPE);
+
+// Cliente WiFi / MQTT
+WiFiClient net;
+PubSubClient client(net);
+
+// =====================
+// VARIABLES A EDITAR
+// =====================
+
+// WiFi
+const char ssid[] = "FAMILIA FRANCO";
+const char pass[] = "1014290398";
+
+// Mosquitto
+#define USER "ironman"
+const char MQTT_HOST[] = "18.207.213.158";
+const int MQTT_PORT = 8082;
+const char MQTT_USER[] = USER;
+const char MQTT_PASS[] = "jarvis123";
+
+// Topics
+const char MQTT_TOPIC_PUB[] = "colombia/cundinamarca/bogota/" USER "/out";
+const char MQTT_TOPIC_SUB[] = "colombia/cundinamarca/bogota/" USER "/in";
+
+// =====================
+// GLOBALES
+// =====================
+
+time_t now;
+
+unsigned long measureTime = 0;
+unsigned long alertTime = 0;
+
+String alert = "";
+float temp = NAN;
+float humi = NAN;
+
+// Evento LED (parpadeo + mensaje en OLED)
+bool ledEventActive = false;
+unsigned long ledEventStartTime = 0;
+unsigned long ledBlinkLastToggle = 0;
+bool ledBlinkState = false;
+
+// =====================
+// MQTT CONNECT
+// =====================
+
+void mqtt_connect() {
+ while (!client.connected()) {
+ Serial.print("MQTT connecting ... ");
+
+ if (client.connect(MQTT_USER, MQTT_USER, MQTT_PASS)) {
+ Serial.println("connected.");
+ client.subscribe(MQTT_TOPIC_SUB);
+ Serial.print("Subscrito a: ");
+ Serial.println(MQTT_TOPIC_SUB);
+ } else {
+ Serial.println("Problema con la conexión, revise los valores de las constantes MQTT");
+ int state = client.state();
+ Serial.print("Código de error = ");
+ alert = "MQTT error: " + String(state);
+ Serial.println(state);
+
+ if (client.state() == MQTT_CONNECT_UNAUTHORIZED) {
+ ESP.deepSleep(0);
+ }
+ delay(5000);
+ }
+ }
+}
+
+void sendSensorData(float temperatura, float humedad) {
+ String data = "{";
+ data += "\"temperatura\": " + String(temperatura, 1) + ", ";
+ data += "\"humedad\": " + String(humedad, 1);
+ data += "}";
+
+ char payload[data.length() + 1];
+ data.toCharArray(payload, data.length() + 1);
+
+ client.publish(MQTT_TOPIC_PUB, payload);
+}
+
+// =====================
+// SENSOR DHT
+// =====================
+
+float readTemperatura() {
+ float t = dht.readTemperature();
+ return t;
+}
+
+float readHumedad() {
+ float h = dht.readHumidity();
+ return h;
+}
+
+bool checkMeasures(float t, float h) {
+ if (isnan(t) || isnan(h)) {
+ Serial.println("Error obteniendo los datos del sensor DHT11");
+ return false;
+ }
+ return true;
+}
+
+// =====================
+// DISPLAY
+// =====================
+
+void startDisplay() {
+ // ESP8266 I2C: SDA=D2 (GPIO4), SCL=D1 (GPIO5)
+ Wire.begin(D2, D1);
+ Wire.setClock(100000);
+
+ u8g2.begin();
+}
+
+void displayNoSignal() {
+ u8g2.clearBuffer();
+ u8g2.setFont(u8g2_font_ncenB08_tr);
+ u8g2.drawStr(10, 18, "No hay senal");
+ u8g2.drawStr(10, 34, "WiFi/MQTT...");
+ u8g2.sendBuffer();
+}
+
+void displayConnecting(const char* ssidName) {
+ u8g2.clearBuffer();
+ u8g2.setFont(u8g2_font_6x12_tf);
+ u8g2.drawStr(0, 14, "Connecting to:");
+ u8g2.drawStr(0, 30, ssidName);
+ u8g2.sendBuffer();
+}
+
+String getHourString() {
+ long long int milli = now + millis() / 1000;
+ struct tm* tinfo = localtime(&milli);
+ String hour = String(asctime(tinfo)).substring(11, 19);
+ return hour;
+}
+
+void renderScreen(const String& message) {
+ u8g2.clearBuffer();
+
+ // Header
+ u8g2.setFont(u8g2_font_6x12_tf);
+ String header = "IOT Sensors " + getHourString();
+ u8g2.drawStr(0, 12, header.c_str());
+ u8g2.drawHLine(0, 14, 128);
+
+ // Medidas
+ u8g2.setFont(u8g2_font_6x12_tf);
+
+ char line1[32];
+ char line2[32];
+
+ // Formato con 1 decimal, evita strings gigantes
+ if (!isnan(temp)) snprintf(line1, sizeof(line1), "T: %.1f C", temp);
+ else snprintf(line1, sizeof(line1), "T: --.- C");
+
+ if (!isnan(humi)) snprintf(line2, sizeof(line2), "H: %.1f %%", humi);
+ else snprintf(line2, sizeof(line2), "H: --.- %%");
+
+ u8g2.drawStr(0, 30, line1);
+ u8g2.drawStr(0, 42, line2);
+
+ // Mensaje / alerta
+ u8g2.drawStr(0, 54, "Msg:");
+ u8g2.setFont(u8g2_font_6x10_tf);
+
+ if (message == "OK") {
+ u8g2.drawStr(40, 54, "OK");
+ } else {
+ // recorta a lo que cabe (pantalla 128; "Evento: LED activado" = 21)
+ String msg = message;
+ if (msg.length() > 21) msg = msg.substring(0, 21);
+ u8g2.drawStr(40, 54, msg.c_str());
+ }
+
+ u8g2.sendBuffer();
+}
+
+// =====================
+// ALERTAS MQTT
+// =====================
+
+String checkAlert() {
+ String message = "OK";
+ if (alert.length() != 0) {
+ message = alert;
+ if ((millis() - alertTime) >= (unsigned long)ALERT_DURATION * 1000UL) {
+ alert = "";
+ alertTime = millis();
+ }
+ }
+ return message;
+}
+
+// Actualiza parpadeo del LED por evento y devuelve mensaje para OLED
+String updateLedEventAndMessage() {
+ if (!ledEventActive) return checkAlert();
+
+ unsigned long elapsed = millis() - ledEventStartTime;
+ if (elapsed >= (unsigned long)LED_EVENT_DURATION * 1000UL) {
+ ledEventActive = false;
+ digitalWrite(LED_PIN, LOW); // LED off (D6: HIGH = on, LOW = off)
+ return checkAlert();
+ }
+
+ if (millis() - ledBlinkLastToggle >= (unsigned long)LED_BLINK_MS) {
+ ledBlinkLastToggle = millis();
+ ledBlinkState = !ledBlinkState;
+ digitalWrite(LED_PIN, ledBlinkState ? HIGH : LOW); // D6: HIGH = encendido
+ }
+ return "Evento: LED activado";
+}
+
+void receivedCallback(char* topic, byte* payload, unsigned int length) {
+ Serial.println("");
+ Serial.println(">>> MENSAJE MQTT RECIBIDO <<<");
+ Serial.print("Topic: ");
+ Serial.println(topic);
+ Serial.print("Payload: ");
+
+ String data = "";
+ for (unsigned int i = 0; i < length; i++) {
+ data += (char)payload[i];
+ }
+ Serial.println(data);
+ Serial.println("================================");
+ Serial.println("");
+
+ if (data.indexOf("ALERT") >= 0) {
+ alert = data;
+ alertTime = millis(); // importante: marca inicio de la alerta
+ }
+
+ if (data.indexOf("LED_ON") >= 0) {
+ Serial.println("");
+ Serial.println("*** EVENTO LED DETECTADO: temperatura_promedio > umbral ***");
+ Serial.println(" -> LED parpadeando y OLED: Evento: LED activado");
+ Serial.println("");
+ ledEventActive = true;
+ ledEventStartTime = millis();
+ ledBlinkLastToggle = millis();
+ ledBlinkState = false;
+ digitalWrite(LED_PIN, LOW); // apagado al inicio (D6: LOW = off)
+ }
+
+}
+
+// =====================
+// WIFI
+// =====================
+
+void checkWiFi() {
+ if (WiFi.status() != WL_CONNECTED) {
+ Serial.print("Checking wifi");
+ while (WiFi.waitForConnectResult() != WL_CONNECTED) {
+ WiFi.begin(ssid, pass);
+ Serial.print(".");
+ displayNoSignal();
+ delay(300);
+ }
+ Serial.println("connected");
+ } else {
+ if (!client.connected()) {
+ mqtt_connect();
+ } else {
+ client.loop();
+ }
+ }
+}
+
+void listWiFiNetworks() {
+ int numberOfNetworks = WiFi.scanNetworks();
+ Serial.println("\nNumber of networks: ");
+ Serial.println(numberOfNetworks);
+ for (int i = 0; i < numberOfNetworks; i++) {
+ Serial.println(WiFi.SSID(i));
+ }
+}
+
+void startWiFi() {
+ WiFi.hostname(USER);
+ WiFi.mode(WIFI_STA);
+ WiFi.begin(ssid, pass);
+
+ Serial.println("\nAttempting to connect to SSID: ");
+ Serial.println(ssid);
+
+ while (WiFi.status() != WL_CONNECTED) {
+ Serial.print(".");
+ delay(1000);
+ }
+ Serial.println("\nconnected!");
+}
+
+// =====================
+// TIME
+// =====================
+
+void setTime() {
+ Serial.print("Setting time using SNTP");
+ configTime(-5 * 3600, 0, "pool.ntp.org", "time.nist.gov");
+
+ now = time(nullptr);
+ while (now < 1510592825) {
+ delay(500);
+ Serial.print(".");
+ now = time(nullptr);
+ }
+ Serial.println("done!");
+
+ struct tm timeinfo;
+ gmtime_r(&now, &timeinfo);
+ Serial.print("Current time: ");
+ Serial.println(asctime(&timeinfo));
+}
+
+// =====================
+// MQTT
+// =====================
+
+void configureMQTT() {
+ client.setServer(MQTT_HOST, MQTT_PORT);
+ client.setCallback(receivedCallback);
+ mqtt_connect();
+}
+
+// =====================
+// MEASURES
+// =====================
+
+void measure() {
+ if ((millis() - measureTime) >= (unsigned long)MEASURE_INTERVAL * 1000UL) {
+ measureTime = millis();
+
+ float t = readTemperatura();
+ float h = readHumedad();
+
+ if (checkMeasures(t, h)) {
+ temp = t;
+ humi = h;
+ sendSensorData(temp, humi);
+ }
+ }
+}
+
+// =====================
+// ARDUINO
+// =====================
+
+void setup() {
+ Serial.begin(115200);
+
+ listWiFiNetworks();
+
+ startDisplay();
+ displayConnecting(ssid);
+
+ startWiFi();
+
+ dht.begin();
+
+ setTime();
+
+ configureMQTT();
+
+ pinMode(LED_PIN, OUTPUT);
+ digitalWrite(LED_PIN, LOW); // LED apagado al inicio (D6)
+
+ measureTime = millis();
+ alertTime = millis();
+}
+
+void loop() {
+ checkWiFi();
+ String message = updateLedEventAndMessage();
+ measure();
+ renderScreen(message);
+}
diff --git a/README.md b/README.md
index 9f61babc..c620d618 100644
--- a/README.md
+++ b/README.md
@@ -173,22 +173,9 @@ void loop() {
## 4. Configuración física (NodeMCU, protoboard y actuadores)
-*Aquí puedes pegar fotos y una breve descripción de tu montaje.*
-
-### 4.1 Esquema de conexiones
-
-Incluye una foto o diagrama de tu configuración (NodeMCU, protoboard, LED, OLED, DHT11).
-
-
-
-**Espacio para imagen 1:** Vista general del NodeMCU y la protoboard.
-
---
-### 4.2 Conexiones utilizadas
+### 4.1 Conexiones utilizadas
| Componente | Conexión en NodeMCU |
|------------|---------------------|
@@ -196,33 +183,3 @@ Ejemplo de cómo insertar una imagen cuando la tengas:
| OLED | D1 (SCL), D2 (SDA), 3V3, GND |
| LED | D6 (ánodo con resistencia ~220Ω) y GND |
-**Espacio para imagen 2:** Detalle del LED en la protoboard (patilla larga/corta y resistencia).
-
----
-
-### 4.3 Evidencia del evento en funcionamiento
-
-**Espacio para imagen 3:** OLED mostrando *"Evento: LED activado"* cuando se dispara el evento.
-
-**Espacio para imagen 4:** LED parpadeando durante el evento (opcional).
-
----
-
-## 5. Redespliegue y pruebas
-
-- La aplicación del servidor (incluido el servicio de control con `evaluate_led_event`) se redesplegó en la infraestructura de nube.
-- Se verificó que:
- - La consulta a la BD devuelve la temperatura promedio por estación.
- - Cuando la temperatura promedio supera 22 °C, se envía `LED_ON` al tópico correcto.
- - El dispositivo recibe el mensaje, parpadea el LED en D6 y muestra *"Evento: LED activado"* en la OLED durante 60 segundos.
-
----
-
-## 6. Repositorio
-
-**Enlace al repositorio GitHub (fork del tutorial):**
-*(pega aquí la URL de tu repositorio)*
-
----
-
-*Documento preparado para el reto de la capa de aplicación (lógica). Las modificaciones descritas corresponden únicamente al nuevo evento con consulta a BD y actuador (LED + OLED).*
From ced1b0436546a0e7e2474ee649d1ead872a50d17 Mon Sep 17 00:00:00 2001
From: HectorFranco-MISO
Date: Fri, 6 Mar 2026 21:15:01 -0500
Subject: [PATCH 31/31] Settings nuevo
---
IOTMonitoringServer/settings.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/IOTMonitoringServer/settings.py b/IOTMonitoringServer/settings.py
index ad43dc16..264220c7 100644
--- a/IOTMonitoringServer/settings.py
+++ b/IOTMonitoringServer/settings.py
@@ -29,8 +29,8 @@
ALLOWED_HOSTS = [
"localhost",
- "3.88.22.107",
- "ec2-3-54-22-107.compute-1.amazonaws.com",
+ "44.201.127.40",
+ "ec2-44-201-127-40.compute-1.amazonaws.com",
"ip-10-0-18-78.ec2.internal",
".compute-1.amazonaws.com", # cualquier EC2 en esa región por hostname
]
@@ -102,7 +102,7 @@
"NAME": "iot_data", # Nombre de la base de datos
"USER": "dbadmin", # Nombre de usuario
"PASSWORD": "uniandesIOT1234*", # Contraseña
- "HOST": "44.203.85.217", # Dirección IP de la base de datos
+ "HOST": "44.203.71.44", # Dirección IP de la base de datos
"PORT": "", # Puerto de la base de datos
}
}
@@ -162,7 +162,7 @@
CRISPY_TEMPLATE_PACK = 'bootstrap4'
# Dirección del bróker MQTT
-MQTT_HOST = "18.207.213.158"
+MQTT_HOST = "44.201.151.218"
# Puerto del bróker MQTT
MQTT_PORT = 8082