Alertas Proativos¶
Este documento descreve o sistema de alertas proativos do EnergIA: os gatilhos, os mecanismos de controle (amostragem e cooldown), os canais de entrega, o modo demonstração e como testar. Atende os requisitos de alerta do briefing do Desafio 6 (seções 4.2, 5 e 11) — em particular as duas vias exigidas: limites pré-configurados pelo usuário e padrões atípicos detectados automaticamente.
1. Gatilhos¶
| # | Alerta | Gatilho | Onde |
|---|---|---|---|
| 1 | ⚠️ Previsão de fatura alta | Novo BillForecast com estimated_cost > 20% acima da média das 3 previsões anteriores do medidor |
analytics/signals.py::alert_on_high_forecast (post_save de BillForecast) |
| 2 | 📈 Limite pré-configurado | Tempo real, por transição: leitura com active_power cruzando o limite Meter.power_alert_threshold_w (abaixo → acima). Rearma quando a potência cai abaixo do limite (Meter.power_alert_active); não usa o cooldown estatístico |
analytics/services.py::evaluate_and_alert_anomaly |
| 3 | 🚨 Pico de consumo (spike) | Leitura atual ≥ 3 desvios-padrão acima da média móvel das últimas 30 leituras (limiar adaptativo; mínimo de 10 leituras de baseline) — os 3 parâmetros são configuráveis por medidor (Sprint 23, ver seção 2) | analytics/services.py::detect_consumption_anomaly |
| 4 | 🔌 Carga fantasma (standby) | Potência mínima das últimas 24h ≥ 1,5× e pelo menos +30 W acima do mínimo dos 7 dias anteriores | analytics/services.py::_detect_standby_anomaly |
| 5 | ⚡ Alta potência na ponta | Degrau NILM ≥ 1000 W detectado durante o posto de ponta de uma tarifa branca ativa — sugere deslocar o uso para o posto mais barato | analytics/services.py::_maybe_alert_high_power_during_peak |
| 6 | 💰 Tarifa branca CELESC | Criação/atualização de preços pela sincronização automática | tariffs/management/commands/sync_white_tariff.py |
| 7 | 🕯️ Falta de energia / 📶 Queda de Wi-Fi | No retorno do medidor: queda consolidada com duração ≥ OUTAGE_ALERT_MIN_MINUTES (padrão 5 min), com data/hora da queda, do retorno e a duração. A causa vem do uptime do boot: baixo = ESP religou do zero (faltou energia); alto = ficou ligado (só perdeu a rede) |
meters/services.py::register_outage_recovery |
Os gatilhos 2–4 são avaliados pelo signal post_save de TelemetryReading (analytics/signals.py::check_consumption_anomaly), ou seja, em tempo real conforme o subscriber MQTT persiste leituras. O gatilho 5 dispara no momento do uso (a detecção de degraus também roda em tempo real via detect_step_realtime — o evento pendente aparece na tela sem esperar o "Analisar este ciclo") e novamente não é reenviado quando a análise em lote reprocessa o mesmo degrau (dedupe por measured_at). Todos os pontos de disparo são envoltos em try/except com log — uma falha no envio nunca interrompe a ingestão de telemetria nem o NILM.
Registro em banco: toda queda ≥ 60 s vira um
OutageEventconsolidado (medidor, início, fim, duração, causa, se alertou) — consultável no admin em "Quedas (energia/rede)" e pelo próprio usuário em/medidores/quedas/(meters:outage_history, Sprint 24), com seletor de período (30/90/365 dias ou todo o período) e indicador de disponibilidade (meters/services.py::get_availability_stats). O dashboard principal também mostra um card resumido de disponibilidade dos últimos 30 dias, linkando para o histórico completo. A causa doDeviceStatusEventde queda é corrigida retroativamente pelo uptime do retorno. Micro-quedas de segundos (flapping de rede) não geram registro consolidado nem alerta; permanecem apenas no log bruto deDeviceStatusEvent.
2. Amostragem e cooldown¶
Para controlar custo computacional e falsos positivos repetidos (critério da seção 11 do briefing):
| Constante | Padrão | Efeito |
|---|---|---|
ANOMALY_EVAL_EVERY_N |
5 | A detecção estatística (spike/standby) roda 1 a cada N leituras (pk % N). O limite pré-configurado (gatilho 2) é verificado em toda leitura — comparação em memória, sem consultas extras |
ANOMALY_COOLDOWN_MINUTES |
30 | Intervalo mínimo entre alertas estatísticos do mesmo medidor (Meter.last_anomaly_alert_at), gatilhos 3–4. O gatilho 2 não espera cooldown: é controlado por transição (Meter.power_alert_active — dispara ao cruzar o limite, rearma ao cair abaixo). Quando o gatilho 2 dispara, ele registra last_anomaly_alert_at para suprimir um alerta estatístico redundante em seguida |
ANOMALY_WINDOW_SIZE / ANOMALY_MIN_SAMPLES / ANOMALY_STD_THRESHOLD |
30 / 10 / 3.0 | Janela, mínimo de amostras e limiar (em σ) da detecção de spike — valor global usado quando o medidor não tem override |
| OUTAGE_ALERT_MIN_MINUTES | 5 | Duração mínima de uma queda (energia/Wi-Fi) para o alerta de retorno — micro-quedas ficam só no log |
ANOMALY_EVAL_EVERY_N, ANOMALY_COOLDOWN_MINUTES e OUTAGE_ALERT_MIN_MINUTES são configuráveis via .env (lidos em core/settings.py); os demais são constantes documentadas em analytics/services.py e meters/services.py.
Sensibilidade por medidor (Sprint 23): Meter.anomaly_window_size/anomaly_min_samples/anomaly_std_threshold (nullable, expostos no formulário de edição do medidor, seção "Sensibilidade de detecção de picos") sobrescrevem ANOMALY_WINDOW_SIZE/ANOMALY_MIN_SAMPLES/ANOMALY_STD_THRESHOLD só para aquele medidor — útil quando um aparelho de variação naturalmente alta (ex.: chuveiro) precisa de um limiar menos sensível que outro medidor mais estável. Vazio (padrão) usa a constante global normalmente.
3. Entrega: canal configurável (WhatsApp/e-mail) + dashboard¶
Todos os gatilhos convergem em assistant/services.py::send_proactive_alert(user, message, alert_type=None). Cada gatilho passa o alert_type correspondente (bill_forecast, power_threshold, spike, standby, peak_tariff, white_tariff_sync, outage — ver tabela da seção 1):
- Preferência (Sprint 23): busca
NotificationPreferencedo usuário (accounts/models.py, 1:1 comUser, criada comget_or_createna primeira visita à tela de perfil). Se o usuário desabilitou essealert_type(notify_<alert_type>=False), a função retorna imediatamente''— nenhum envio e nenhum registro emAssistantMessagepara esse disparo. Sem preferência salva (usuário nunca configurou nada), o comportamento é idêntico ao anterior à Sprint 23: WhatsApp, todos os tipos habilitados. - Canal:
NotificationPreference.channeldefinewhatsapp/email/both(padrãowhatsapp). O número WhatsApp é resolvido pela últimaAssistantMessagedo usuário comphone_number— só é usado se o usuário já conversou com o bot alguma vez. Envio por e-mail (_send_alert_email) usadjango.core.mail.send_mailcom oEMAIL_BACKEND/SMTP já configurado emcore/settings.py(o mesmo usado pelo reset de senha), removendo os marcadores markdown do WhatsApp (*) da mensagem; nunca levanta exceção — falha de e-mail é só logada. - Sempre (quando o tipo não foi suprimido pela preferência) registra
AssistantMessage(direction='outbound', is_proactive_alert=True)— independente do canal escolhido, é o log interno que alimenta o dashboard e o histórico, não uma "entrega" selecionável. - Retorna o número WhatsApp usado (string vazia se nenhum foi encontrado, ou se o alerta foi suprimido pela preferência) — usado por chamadores que precisam saber se a entrega teve destino real, como o envio sob demanda da seção 3.1.
Configuração pelo usuário: tela de Perfil (/perfil/), seção "Notificações" — canal + um checkbox por tipo de alerta (templates/accounts/profile.html, accounts:notification_preference_update).
O registro alimenta dois pontos da interface web:
- O card "Último alerta" no dashboard (dashboard/views.py → assistant/services.py::get_recent_alerts, exibe apenas o alerta mais recente) — garante que o alerta chegue a quem nunca usou o WhatsApp; o histórico completo fica na página do assistente.
- O histórico do assistente (/assistente/historico/).
3.1 Envio sob demanda: simulação de tarifa branca¶
Diferente dos gatilhos 1–7 (automáticos, disparados por signals/comandos), a tela de Simulação de Custo por Posto Horário (/tarifas/simulacao/) tem um botão "Enviar via WhatsApp" que envia o resultado já calculado a pedido do próprio usuário — mesmo pipeline de entrega (send_proactive_alert), mas iniciado por uma ação humana, não por uma condição do sistema. Por isso chama send_proactive_alert sem alert_type: não é filtrável pela preferência de notificação (o usuário clicou o botão agora, a intenção é explícita) e sempre vai por WhatsApp, independente do channel configurado.
tariffs/services.py::send_simulation_whatsapp(user, tariff, result)monta a mensagem (breakdown por posto, total, melhor/pior caso, economia potencial) e retorna'sent'/'no_phone'/'error'para feedback viadjango.contrib.messagesna mesma tela — nunca levanta exceção, a simulação exibida não pode quebrar por causa do WhatsApp.- Formatação numérica: a mensagem usa exclusivamente vírgula decimal (
tariffs/services.py::_br_num/_brl), nunca o ponto padrão do Python — texto enviado a um usuário real não pode misturar as duas convenções (ex.: "10.5 kWh" ao lado de "R$ 21,00" no mesmo parágrafo). Na tela, os números já saem formatados corretamente porquefloatformatdo Django é locale-aware comLANGUAGE_CODE = 'pt-br'; o texto do WhatsApp é montado à mão (f-strings), por isso precisa do helper. CostSimulationView.form_validdistingue as duas ações do mesmo formulário pelo campo ocultoaction(simulatevssend_whatsapp), sem view nem URL extra.
4. Modo demonstração¶
Para a demo ao vivo recomendada pelo briefing ("ligar um equipamento de alta potência e ver o alerta disparar"), os padrões de produção atrapalham: a amostragem atrasa o disparo e o cooldown de 30 min impede repetir a demonstração. Duas ferramentas:
# .env — disparo imediato e repetível
ANOMALY_EVAL_EVERY_N=1
ANOMALY_COOLDOWN_MINUTES=0
# Alternativa sem mexer no .env: zerar o cooldown entre demonstrações
python manage.py reset_anomaly_cooldown # todos os medidores
python manage.py reset_anomaly_cooldown --meter 3 # um medidor específico
Em produção (Docker), mudar o .env exige recriar o container (docker compose -f docker-compose.prod.yml up -d web mqtt_subscriber); o comando de reset roda direto: docker compose -f docker-compose.prod.yml exec web python manage.py reset_anomaly_cooldown.
O gatilho 2 (limite pré-configurado) é o mais confiável para demo: configure, por exemplo, 1000 W no medidor e ligue um chuveiro — dispara na primeira leitura acima do limite, sem depender de baseline estatístico nem de cooldown. Para repetir a demonstração, basta desligar e religar a carga (o alerta rearma quando a potência cai abaixo do limite); o
reset_anomaly_cooldowne o.envde demo continuam necessários apenas para os gatilhos estatísticos (3–4).
5. Como testar¶
Automatizado — python manage.py test analytics cobre detecção (spike/standby), limite pré-configurado, amostragem e cooldown (DetectConsumptionAnomalyTest, DetectStandbyAnomalyTest, EvaluateAndAlertAnomalyTest, PowerThresholdAlertTest), sempre com send_proactive_alert mockado.
Ponta a ponta — no Django shell:
from accounts.models import User
from assistant.services import send_proactive_alert
user = User.objects.get(email='...')
send_proactive_alert(user, '🧪 Teste de alerta EnergIA')
Se chegar no WhatsApp, toda a cadeia de entrega está OK. O que foi disparado (com ou sem WhatsApp) é auditável em:
from assistant.models import AssistantMessage
AssistantMessage.objects.filter(is_proactive_alert=True).order_by('-created_at')
Pré-requisitos em produção: o mqtt_subscriber precisa estar rodando (é ele quem salva as leituras que disparam os gatilhos 2–4) e, para receber no WhatsApp, o usuário precisa ter trocado ao menos uma mensagem com o bot.
6. Cenários de teste pré-definidos e falsos positivos¶
A seção 11 do briefing avalia o alerta pela "corretude do disparo em cenários de teste previamente definidos" e pela "taxa de falsos positivos". Os cenários formais são estes (todos reproduzíveis no medidor real com o modo demo ativo):
| # | Cenário | Como reproduzir | Resultado esperado |
|---|---|---|---|
| C1 | Spike estatístico | Consumo estável por ≥ 30 leituras; ligar carga que eleve a potência ≥ 3σ acima da média (ex.: chuveiro) | Alerta 🚨 em até ANOMALY_EVAL_EVERY_N leituras |
| C2 | Sem falso positivo em operação normal | Rotina normal da casa por 24h, sem cargas novas | Nenhum alerta estatístico |
| C3 | Limite pré-configurado | Configurar limite abaixo de uma carga conhecida; ligar a carga | Alerta 📈 na primeira leitura acima do limite (tempo real, sem cooldown) |
| C4a | Sem reenvio com carga ligada | Manter a carga de C3 ligada acima do limite | Nenhum reenvio enquanto a potência não cair abaixo do limite |
| C4b | Rearme por transição | Desligar a carga de C3 (potência cai abaixo do limite) e religar | Novo alerta 📈 imediato, mesmo dentro dos 30 min |
| C4c | Cooldown estatístico | Repetir C1 dentro da janela de cooldown (produção: 30 min) | Segundo alerta estatístico suprimido |
| C5 | Carga fantasma | Deixar um aparelho de standby elevado (≥ +30 W e ≥ 1,5× o mínimo histórico) ligado por 24h | Alerta 🔌 |
| C6 | Alta potência na ponta | Com tarifa branca ativa, ligar carga ≥ 1000 W dentro do posto de ponta | Alerta ⚡ com comparação de preço entre postos |
Os cenários C1–C4c têm equivalentes automatizados na suíte (analytics/tests.py); C5 e C6 são cobertos por DetectStandbyAnomalyTest e pelos testes de _maybe_alert_high_power_during_peak com dados sintéticos.
Medição da taxa de falsos positivos em produção: todo disparo fica auditável em AssistantMessage(is_proactive_alert=True). O procedimento é revisar semanalmente os alertas do período e classificar cada um como procedente (havia causa real: aparelho ligado, standby, etc.) ou falso positivo:
from assistant.models import AssistantMessage
AssistantMessage.objects.filter(is_proactive_alert=True).order_by('-created_at').values_list('created_at', 'content')
Taxa = falsos positivos / total de alertas do período. Registrar o resultado aqui a cada revisão:
| Período | Alertas | Falsos positivos | Taxa | Observações |
|---|---|---|---|---|
| — | — | — | — | — |
Limite conhecido: com uma única residência instrumentada, a amostra é pequena — declarar isso à banca em vez de extrapolar (a honestidade sobre os limites da PoC é critério positivo, seção 10 do briefing).
7. Referências¶
analytics/signals.py— signals de disparo (forecast e leitura).analytics/services.py— detecção, limiares e mensagens (seção "Detecção de anomalia em tempo real").assistant/services.py— entrega (send_proactive_alert,get_recent_alerts).analytics/management/commands/reset_anomaly_cooldown.py— reset do cooldown.PRD.md, Sprints 18 e 22 — histórico de implementação.- Briefing do Desafio 6 (
arquivos/Desafio06_briefing_MedidorAssistente.docx.pdf), seções 4.2, 5 e 11.