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/IOTMonitoringServer/settings.py b/IOTMonitoringServer/settings.py
index 114ccc2f..264220c7 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", "ip.maquina.visualizador"]
+ALLOWED_HOSTS = [
+ "localhost",
+ "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
+]
# Application definition
@@ -96,7 +102,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": "44.203.71.44", # Dirección IP de la base de datos
"PORT": "", # Puerto de la base de datos
}
}
@@ -156,7 +162,7 @@
CRISPY_TEMPLATE_PACK = 'bootstrap4'
# Dirección del bróker MQTT
-MQTT_HOST = "ip.maquina.mqtt"
+MQTT_HOST = "44.201.151.218"
# Puerto del bróker MQTT
MQTT_PORT = 8082
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..c620d618
--- /dev/null
+++ b/README.md
@@ -0,0 +1,185 @@
+# 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)
+
+---
+
+### 4.1 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 |
+
diff --git a/control/management/commands/check_led_event.py b/control/management/commands/check_led_event.py
new file mode 100644
index 00000000..a3cecc84
--- /dev/null
+++ b/control/management/commands/check_led_event.py
@@ -0,0 +1,135 @@
+"""
+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)
+
+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
+from django.utils import timezone
+from receiver.models import Data, Station
+
+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(
+ '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). Use --send --force para probar sin esperar 22°C.'
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ '--send',
+ action='store_true',
+ 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=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'))
+
+ 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))
+ send_led_on_to_topics(topics)
+ self.stdout.write(self.style.SUCCESS('LED_ON enviado. Revisa el NodeMCU.'))
+ return
+
+ 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('')
+ 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
+ ]
+ 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).'))
diff --git a/control/monitor.py b/control/monitor.py
index 43d7af0c..b81cd8b5 100644
--- a/control/monitor.py
+++ b/control/monitor.py
@@ -2,13 +2,16 @@
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
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():
@@ -19,7 +22,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,40 +54,104 @@ 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))
- client.publish(topic, message)
+ if not mqtt_connected:
+ try:
+ client.reconnect()
+ 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:
+ print(timezone.now(), "NO enviado (desconectado): alert a", topic)
alerts += 1
print(len(aggregation), "dispositivos revisados")
print(alerts, "alertas enviadas")
+# 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.
+ """
+ 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=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'))
+
+ 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)
+ if not mqtt_connected:
+ try:
+ client.reconnect()
+ 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
+ else:
+ print(timezone.now(), "NO enviado LED_ON (desconectado):", topic)
+
+ print(sent, "comandos LED_ON enviados")
+
+
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
@@ -94,19 +161,34 @@ 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():
'''
- 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)
- 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:
+ if not mqtt_connected:
+ try:
+ client.reconnect()
+ time.sleep(1)
+ except Exception:
+ pass
schedule.run_pending()
time.sleep(1)
diff --git a/receiver/management/commands/fix_placeholder_location.py b/receiver/management/commands/fix_placeholder_location.py
new file mode 100644
index 00000000..69a100eb
--- /dev/null
+++ b/receiver/management/commands/fix_placeholder_location.py
@@ -0,0 +1,93 @@
+"""
+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
+ 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 = '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)')
+ 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']
+
+ placeholder_locations = Location.objects.filter(
+ city__name='ciudad',
+ state__name='estado',
+ country__name='pais'
+ ).select_related('city', 'state', 'country')
+
+ if not placeholder_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={})
+
+ # 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:
+ 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)))
+ self.stdout.write('Creada Location {}, {}, {}'.format(city_name, state_name, country_name))
+
+ # 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()
+ 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. {} estación(es) reasignada(s) a {}, {}, {}. Recarga el mapa.'.format(moved, city_name, state_name, country_name)))
diff --git a/receiver/mqtt.py b/receiver/mqtt.py
index 15fd8136..db3f948b 100644
--- a/receiver/mqtt.py
+++ b/receiver/mqtt.py
@@ -1,35 +1,22 @@
-from datetime import datetime
from . import utils
import json
-import os
+from django.utils import timezone
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()
+ time = timezone.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,55 @@ 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, userdata, rc):
+ print("⚠️ Desconectado:", mqtt.connack_string(rc))
+ print("Intentando reconectar...")
-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()
+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)
+ # ✅ 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
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
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)
diff --git a/viewer/templates/historical.html b/viewer/templates/historical.html
index ca9a51d2..29c16d7f 100644
--- a/viewer/templates/historical.html
+++ b/viewer/templates/historical.html
@@ -27,13 +27,17 @@ Datos históricos
Descarga los datos de las estaciones
de monitoreo.
+
+
+ Ú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/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 %}