Pular para conteúdo

NILM — Desagregação Não Intrusiva de Cargas

Este documento descreve o algoritmo de NILM (Non-Intrusive Load Monitoring) do EnergIA, a lógica de recomendação de horário de uso derivada dele, o diagrama de arquitetura de ponta a ponta e a metodologia de avaliação de acurácia — item exigido pelo briefing do Desafio 6 ("relatório de acurácia obtida").


1. Abordagem: detecção de eventos (event-based)

O briefing do Desafio 6 recomenda, para uma PoC, a "análise de transições de potência ativa (event-based), que identifica liga/desliga de equipamentos por saltos característicos no sinal agregado" — é exatamente a técnica implementada em analytics/services.py::detect_power_steps.

A cada leitura consecutiva de TelemetryReading.active_power, calcula-se o delta absoluto. Deltas acima de um limiar configurável (200 W por padrão) são registrados como um degrau de potência (PowerStepEvent), com:

  • delta_w — magnitude do salto em Watts.
  • delta_a — magnitude do salto de corrente em Amperes (informativo).
  • duration_s — para degraus de subida, o tempo até a potência retornar perto da base anterior (estimativa de quanto tempo o aparelho ficou ligado), limitado a uma janela de 24h para evitar custo O(n²).
  • hour_of_day — hora local em que o evento ocorreu (0-23).

Nenhum degrau é identificado automaticamente de forma definitiva: todo evento novo nasce com status='pending' e appliance=None. A classificação (seção 2) só preenche um palpite (suggested_appliance_name) para pré-preencher o formulário de identificação manual — os totais de consumo por aparelho (LoadDisaggregation) só refletem aparelhos que o usuário confirmou manualmente.


2. Classificação em três camadas

analytics/services.py::_classify_event tenta, em ordem, da mais específica à mais genérica:

Camada A — Árvore de decisão por medidor

Um DecisionTreeClassifier (scikit-learn) treinado exclusivamente com os eventos confirmados manualmente (status='confirmed') daquele medidor, usando [delta_w, duration_s, hour_of_day] como features e o nome do aparelho como rótulo.

Exige no mínimo 6 eventos confirmados cobrindo pelo menos 2 aparelhos distintos; abaixo disso, a camada é pulada. A predição só é aceita se a probabilidade máxima for ≥ 60% — caso contrário, cai para a camada B.

Por que só eventos confirmados treinam o modelo: eventos auto (classificados automaticamente) nunca entram no treino. Se entrassem, um erro de classificação se tornaria dado de treino para o próximo erro — um loop de auto-reforço já identificado e corrigido no projeto (ver feedback_django_ops do histórico de decisões).

Camada B — Correspondência com o histórico confirmado

_match_known_appliance compara o delta_w do novo evento contra cada evento já confirmado do medidor (não uma média por aparelho), aceitando o aparelho do evento histórico mais próximo dentro de 15% de tolerância. Funciona desde a primeira confirmação — ao contrário da árvore de decisão, que exige 6+ amostras.

Camada C — Assinaturas fixas

Faixas de potência típicas e conhecidas (KNOWN_SIGNATURES): Chuveiro Elétrico (3000-8000 W), Ar-Condicionado (800-4000 W), Forno Elétrico (1000-3500 W), Geladeira (100-400 W). Só retorna um palpite quando o delta_w cai em exatamente uma faixa — deltas ambíguos (ex.: 1500 W, que cabe tanto em Ar-Condicionado quanto em Forno Elétrico) retornam None deliberadamente, em vez de arriscar um palpite confiante e possivelmente errado.


3. Metodologia de avaliação de acurácia

Comando: python manage.py evaluate_nilm [--meter ID] [--test-ratio 0.3] [--min-events 6]

Para cada medidor com pelo menos --min-events eventos confirmados:

  1. Os eventos confirmados são ordenados cronologicamente e divididos em treino (mais antigos) e teste (mais recentes), na proporção 1 - test_ratio / test_ratio (padrão 70%/30%).
  2. Para cada evento do conjunto de teste, a pipeline de 3 camadas é executada usando apenas o conjunto de treino como histórico disponível (_classify_event(..., history=train)) — o rótulo verdadeiro do evento de teste nunca é usado para treinar ou como referência de correspondência, evitando vazamento.
  3. Reporta acurácia geral e, por aparelho, precisão/recall/F1/suporte (sklearn.metrics.precision_recall_fscore_support).
$ python manage.py evaluate_nilm

Casa Principal — 14 evento(s) de treino / 6 de teste
Acurácia geral: 83.3%  (6 eventos de teste)
Aparelho                    Precisão     Recall       F1  Suporte
Chuveiro Elétrico              100.0%    100.0%   100.0%        3
Geladeira                       66.7%     66.7%    66.7%        3

=== Geral (todos os medidores avaliados) ===
Acurácia geral: 83.3%  (6 eventos de teste)

Nota de honestidade sobre os limites da PoC: a saída acima é ilustrativa (dados sintéticos de teste automatizado), não uma métrica de produção — o EnergIA hoje tem uma única residência instrumentada, e a Camada A (árvore de decisão) só entra em ação a partir de 6 eventos confirmados por medidor cobrindo 2+ aparelhos. Com poucos rótulos locais, a avaliação real tende a se apoiar mais nas Camadas B/C. Rode o comando periodicamente à medida que mais eventos forem confirmados para acompanhar a acurácia real evoluir; números fabricados não substituem essa medição contínua.

Limitação conhecida do split cronológico

Se o usuário confirmar todos os eventos de um aparelho em um período e de outro aparelho só depois, o split 70/30 pode concentrar um aparelho inteiro no treino e outro no teste, produzindo um relatório de acurácia sobre um subconjunto de aparelhos apenas naquela rodada. Isso é esperado e documentado — não é um bug — mas reforça a recomendação de rodar o comando regularmente conforme novos eventos forem confirmados.


4. Recomendação de horário de uso (Sprint 17 do PRD)

A partir da tarifa branca (ver docs/hardware.md... não — ver seção de tarifas do PRD, Sprint 16) e dos aparelhos já confirmados pelo NILM, analytics/services.py::suggest_best_usage_window(appliance, tariff) calcula:

  1. O posto horário mais barato (recommended_slot) e o mais caro (worst_slot) da tarifa branca ativa.
  2. O consumo estimado por uso (appliance.typical_power / 1000 × 1 hora, aproximação documentada — ciclos reais de chuveiro/máquina de lavar costumam ficar próximos disso).
  3. A economia estimada em R$: custo no posto mais caro − custo no posto recomendado.

get_usage_suggestions(meter, tariff) aplica isso a todos os aparelhos conhecidos do medidor, ordenado pela maior economia — é o que alimenta o card "Recomendação de horário de uso" na tela de Desagregação (web) e a seção equivalente do contexto do assistente (WhatsApp/chat), permitindo respostas como "qual o melhor horário para usar o chuveiro?" a partir dos mesmos dados, sem um intent matcher dedicado — o LLM já responde consultando o contexto textual, no mesmo padrão usado para tarifas e faturas.

Como reforço proativo (opcional, também implementado): quando um degrau de alta potência (≥ 1000 W) é detectado durante o posto de ponta de uma tarifa branca ativa, _maybe_alert_high_power_during_peak envia um alerta via WhatsApp sugerindo o deslocamento para o posto mais barato. Este e os demais alertas proativos (anomalia, limite configurado pelo usuário, previsão de fatura alta) estão documentados em docs/alertas.md — incluindo o card "Último alerta" no dashboard e o modo demonstração.


5. Diagrama de arquitetura (ponta a ponta)

flowchart LR
    subgraph Edge [Camada Edge — C++ / firmware]
        ESP[ESP32 + CT clamp + ZMPT101B]
    end

    subgraph Broker [Comunicação]
        HMQ[(HiveMQ Cloud — MQTTS 8883)]
    end

    subgraph Processamento [Processamento — Django]
        SUB[Subscriber MQTT]
        TEL[(TelemetryReading — TimescaleDB)]
        NILM[detect_power_steps + classificação 3 camadas]
        EVT[(PowerStepEvent / Appliance / LoadDisaggregation)]
        ANOM[detect_consumption_anomaly]
        TAR[Tarifa branca: get_period_for_datetime]
        REC[suggest_best_usage_window]
    end

    subgraph Aplicacao [Aplicação]
        WEB[Dashboard web / app Flet]
        BOT[Assistente WhatsApp / chat — RAG]
    end

    ESP -- MQTT/TLS --> HMQ
    HMQ --> SUB
    SUB --> TEL
    TEL --> NILM
    NILM --> EVT
    TEL --> ANOM
    EVT --> REC
    TAR --> REC
    ANOM -- alerta proativo --> BOT
    REC -- alerta proativo --> BOT
    ANOM -- card Ultimo alerta --> WEB
    EVT --> WEB
    REC --> WEB
    TEL --> WEB
    WEB --> BOT

A camada Edge (aquisição/firmware) é responsabilidade de uma frente de hardware separada (ver docs/hardware.md) — este repositório Django consome os dados já processados (Vrms, Irms, potência, kWh) publicados via MQTT.


6. Referências

  • analytics/services.py — implementação completa (detecção, classificação, recomendação, anomalia).
  • analytics/management/commands/evaluate_nilm.py — comando de avaliação.
  • EnergIA_vs_Desafio6_Analise.docx (pasta arquivos/, não versionada) — mapeamento contra os requisitos do Desafio 6.
  • PRD.md, seção 13-A (Sprints 16-21) — plano de adequação ao desafio.