Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
74c4c40
First commit
HectorFranco-MISO Feb 25, 2026
1c256b3
Second commit
HectorFranco-MISO Feb 25, 2026
b697ed3
HOST BD
HectorFranco-MISO Feb 25, 2026
4b5d597
HOST BD Public IP
HectorFranco-MISO Feb 25, 2026
b26b62a
Ajustar mqtt
HectorFranco-MISO Feb 25, 2026
e572688
Ajustar mqtt a /
HectorFranco-MISO Feb 25, 2026
4689f80
Ajustar mqtt a /
HectorFranco-MISO Feb 25, 2026
8dda8c9
Ajuste variables
HectorFranco-MISO Feb 25, 2026
2055fa5
Ajuste variables
HectorFranco-MISO Feb 26, 2026
f6c5961
Ajuste variables
HectorFranco-MISO Feb 26, 2026
ea1e26a
Ajuste variables
HectorFranco-MISO Feb 26, 2026
57d892a
Cambio en código para el reto S6
HectorFranco-MISO Feb 27, 2026
958d1b7
Ajuste temperatura
HectorFranco-MISO Feb 27, 2026
d0fd4d8
chequear el evento del led
HectorFranco-MISO Feb 27, 2026
1c58b00
Corregir Timezone
HectorFranco-MISO Feb 27, 2026
6ca85dc
Ajuste a 2 minutos
HectorFranco-MISO Feb 27, 2026
ef70c0c
Conexión a NodeMCU - Broker
HectorFranco-MISO Feb 27, 2026
36f59c2
Conexión de IOT Alert
HectorFranco-MISO Feb 27, 2026
9fa1267
Prueba de LED
HectorFranco-MISO Feb 28, 2026
3bb110b
Prueba led
HectorFranco-MISO Feb 28, 2026
e8b3694
Viewer
HectorFranco-MISO Feb 28, 2026
adcc908
fix superuser
HectorFranco-MISO Feb 28, 2026
ff6d749
Fix views
HectorFranco-MISO Feb 28, 2026
b5f631e
fix location
HectorFranco-MISO Feb 28, 2026
d6aa41f
Fix location 2
HectorFranco-MISO Feb 28, 2026
6079db1
Coordenadas
HectorFranco-MISO Feb 28, 2026
6c0fadd
Ajuste de rema
HectorFranco-MISO Feb 28, 2026
be052f8
Download data
HectorFranco-MISO Feb 28, 2026
e6cae9e
Despliegue
HectorFranco-MISO Feb 28, 2026
3ceee55
commit final
HectorFranco-MISO Feb 28, 2026
ced1b04
Settings nuevo
HectorFranco-MISO Mar 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
410 changes: 410 additions & 0 deletions IOTDeviceScript/IOTDeviceScript.ino

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions IOTMonitoringServer/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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
Expand Down
185 changes: 185 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 |

135 changes: 135 additions & 0 deletions control/management/commands/check_led_event.py
Original file line number Diff line number Diff line change
@@ -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).'))
Loading