Pular para conteúdo

Modelos de Dados

BaseModel (core)

Todo model herda de BaseModel, que adiciona dois campos automáticos:

Campo Tipo Descrição
created_at DateTimeField Preenchido automaticamente na criação
updated_at DateTimeField Atualizado automaticamente a cada save

User (accounts)

Usuário customizado que substitui o model padrão do Django. O campo de autenticação é o e-mail (sem username).

Campo Tipo Observação
email EmailField Único; usado como USERNAME_FIELD
first_name CharField
last_name CharField
whatsapp_number CharField Número principal (formato: 55DDD9XXXXXXXX)
whatsapp_number_2 CharField Número sobressalente 1 (opcional)
whatsapp_number_3 CharField Número sobressalente 2 (opcional)
whatsapp_jid CharField JID interno do WhatsApp (resolvido via LID)
viewing ForeignKey('self') Nullable, on_delete=SET_NULL, related_name='viewers'. Se definido, esta conta é uma visualizadora somente-leitura dos dados do usuário apontado
consented_nilm_training_at DateTimeField, opcional Momento em que o usuário autorizou usar os próprios aparelhos confirmados para treinar o classificador NILM (LGPD). None = não consentiu
is_active BooleanField
is_staff BooleanField
created_at / updated_at DateTimeField De BaseModel

Até 3 números WhatsApp podem ser vinculados por usuário. O bot identifica o remetente em qualquer um dos três e rotula as mensagens no histórico como Principal, Sobressalente 1 ou Sobressalente 2.

Contas visualizadoras: a property User.effective_owner (self.viewing or self) resolve, em qualquer parte do sistema, de quem são os dados que a conta logada deve enxergar — para uma conta normal é ela mesma; para uma conta visualizadora, é o viewing. Toda query de dados do usuário (web, API, assistente) usa owner=<algo>.effective_owner. Um usuário pode ter múltiplos visualizadores (user.viewers.all()). Ver PRD.md seção 6.11 para o fluxo completo.


NotificationPreference (accounts)

Preferências de notificação proativa do usuário — um registro por owner (OneToOneField).

Campo Tipo Observação
owner OneToOneField → User related_name='notification_preference'
channel CharField whatsapp, email ou both (padrão: whatsapp)
notify_bill_forecast BooleanField Previsão de fatura alta (padrão: True)
notify_power_threshold BooleanField Limite de potência pré-configurado (padrão: True)
notify_spike BooleanField Pico de consumo (padrão: True)
notify_standby BooleanField Carga fantasma / standby (padrão: True)
notify_peak_tariff BooleanField Alta potência no posto de ponta (padrão: True)
notify_white_tariff_sync BooleanField Sincronização de tarifa branca (padrão: True)
notify_outage BooleanField Queda de energia/rede (padrão: True)

Meter (meters)

Representa um medidor IoT registrado pelo usuário.

Campo Tipo Observação
owner FK → User Usuário dono do medidor
name CharField Apelido/nome do medidor
mqtt_id CharField Identificador MQTT único (ex: medidor_01)
location CharField Localização (ex: Quadro geral)
is_online BooleanField Atualizado pelo LWT/boot via MQTT
last_seen_at DateTimeField Último evento de status recebido
last_anomaly_alert_at DateTimeField Cooldown dos alertas de anomalia/limite — evita reenvio a cada leitura (ver docs/alertas.md)
power_alert_threshold_w PositiveIntegerField Limite de potência (W) configurado pelo usuário no formulário do medidor — dispara alerta quando ultrapassado; vazio desativa
power_alert_active BooleanField Estado do alerta de limite: True enquanto a potência segue acima do limite. Dispara na transição abaixo→acima e rearma quando cai abaixo
anomaly_window_size PositiveIntegerField, opcional Nº de leituras recentes usadas como base para detectar picos. Vazio usa o padrão do sistema (30)
anomaly_min_samples PositiveIntegerField, opcional Leituras mínimas necessárias antes de avaliar picos neste medidor. Vazio usa o padrão do sistema (10)
anomaly_std_threshold DecimalField, opcional Desvios-padrão acima da média que disparam o alerta de pico. Menor = mais sensível. Vazio usa o padrão do sistema (3.0)

TelemetryReading (telemetry)

Cada leitura enviada pelo ESP32. Tabela convertida em hypertable do TimescaleDB, particionada por measured_at.

Campo Tipo Observação
meter FK → Meter
voltage_rms FloatField Tensão eficaz (V)
current_rms FloatField Corrente eficaz (A)
active_power FloatField Potência ativa (W)
power_factor FloatField Fator de potência (cos φ)
energy_kwh FloatField Energia acumulada (kWh)
measured_at DateTimeField Timestamp da medição (chave de partição)

DeviceStatusEvent (meters)

Registra cada evento de status (boot ou desconexão) do medidor.

Campo Tipo Observação
meter FK → Meter
state CharField 'online' ou 'offline'
uptime_seconds IntegerField Tempo ligado antes do evento
reset_reason CharField Motivo do reset reportado pelo firmware
disconnect_cause CharField 'power_outage' ou 'network_failure' (inferido)
occurred_at DateTimeField Momento do evento

A lógica de classificação (disconnect_cause) fica na service layer de telemetry: uptime baixo logo após retorno → queda de energia; uptime alto contínuo → falha de rede.


OutageEvent (meters)

Queda consolidada (energia ou conectividade): um registro por período offline→online, criado no retorno. Enquanto DeviceStatusEvent é o log bruto de cada evento de status, OutageEvent é o evento consultável — data/hora da queda, retorno, duração e causa — usado nas telas e alertas de queda.

Campo Tipo Observação
meter FK → Meter related_name='outages'
started_at DateTimeField Momento da queda (último offline)
restored_at DateTimeField Momento do retorno (online estabilizado)
duration_seconds PositiveIntegerField Duração da queda
cause CharField power_outage, network_failure ou '' (não determinado)
alert_sent BooleanField Se o alerta proativo já foi enviado para esta queda

Tariff (tariffs)

Tarifa cadastrada pelo usuário, seguindo as regras da ANEEL. Pode ser uma tarifa por bandeira (padrão) ou uma tarifa branca por postos horários (is_white=True), modalidades mutuamente exclusivas por registro.

Campo Tipo Observação
owner FK → User
name CharField Identificador amigável
price_per_kwh DecimalField Valor do kWh (R$) — usado quando is_white=False
flag_color CharField Bandeira tarifária (verde, amarela, vermelha 1/2)
flag_extra_cost DecimalField Adicional da bandeira (R$/100 kWh)
taxes_percent DecimalField Impostos e encargos (%)
is_active BooleanField Tarifa usada nos cálculos
is_white BooleanField Tarifa branca (postos ponta/intermediário/fora de ponta, RN ANEEL nº 1.000/2021) em vez de bandeira. Quando True, o preço vem dos TariffTimeSlot associados, não de price_per_kwh
rate_source CharField, opcional Fonte dos valores tarifários (ex.: Resolução Homologatória ANEEL ou site da distribuidora) — obrigatório quando is_white=True
rate_source_date DateField, opcional Data de consulta dos valores — obrigatório quando is_white=True

Ao confirmar uma fatura, o sistema cria ou atualiza automaticamente a tarifa "CELESC Residencial" com price_per_kwh_billed, flag_color_billed e taxes_percent_billed da fatura, ativando-a como tarifa corrente. O usuário não precisa cadastrar tarifa manualmente. Para tarifa branca, tariffs/management/commands/sync_white_tariff.py sincroniza os postos horários da CELESC automaticamente.


TariffTimeSlot (tariffs)

Posto horário de uma tarifa branca (Tariff.is_white=True). Os três postos são fixos por definição regulatória — ponta e intermediário só valem em dias úteis; fora de ponta é o padrão em fins de semana e feriados nacionais — regra aplicada em tariffs.services.get_period_for_datetime, não configurável por posto.

Campo Tipo Observação
tariff FK → Tariff Tarifa branca à qual o posto pertence (related_name='time_slots')
period_type CharField peak (ponta), intermediate (intermediário) ou off_peak (fora de ponta)
start_time / end_time TimeField Faixa horária do posto. Se end_time < start_time, o intervalo cruza a meia-noite
price_per_kwh DecimalField (5 casas) Preço do posto — precisão maior que Tariff.price_per_kwh pois tarifas ANEEL/distribuidora costumam ser publicadas com 5 casas decimais (ex.: R$ 1,17082/kWh)

Não há unique_together por (tariff, period_type) sozinho: algumas distribuidoras (ex.: CELESC) dividem o posto intermediário em dois intervalos, um de cada lado da ponta. O unique_together real é (tariff, period_type, start_time, end_time), evitando apenas linhas literalmente duplicadas.


BillUpload (tariffs)

Fatura enviada pelo usuário (upload de PDF/imagem) para extração automática de dados e comparação com o que o sistema estimou.

Campo Tipo Observação
owner FK → User related_name='bill_uploads'
meter FK → Meter related_name='bill_uploads'
bill_file FileField Arquivo enviado (bills/%Y/%m/)
reference_month DateField Normalizado para o dia 1 do mês de referência
cycle_start / cycle_end DateField, opcional Datas de leitura extraídas da fatura
status CharField uploaded (enviada) ou confirmed (confirmada)
billed_kwh DecimalField, opcional Consumo faturado (kWh), extraído da fatura
billed_total DecimalField, opcional Valor total faturado (R$)
price_per_kwh_billed DecimalField, opcional Preço do kWh extraído da fatura
flag_color_billed CharField, opcional Bandeira tarifária extraída da fatura
taxes_percent_billed DecimalField, opcional Impostos/encargos extraídos da fatura
next_reading_date DateField, opcional Data da próxima leitura extraída da fatura
system_kwh DecimalField, opcional Consumo estimado pelo sistema no mesmo ciclo
system_estimated_cost DecimalField, opcional Custo estimado pelo sistema no mesmo ciclo
kwh_discrepancy_pct FloatField, opcional Diferença percentual entre system_kwh e billed_kwh
cost_discrepancy_pct FloatField, opcional Diferença percentual entre system_estimated_cost e billed_total
parse_notes TextField Observações da extração automática (ex.: campos não encontrados)

Ao confirmar (status='confirmed'), o sistema cria/atualiza automaticamente a tarifa "CELESC Residencial" com os valores _billed — ver nota em Tariff acima. Ver docs/lgpd.md para retenção do arquivo enviado.


BillForecast (analytics)

Previsão de fatura gerada pelo módulo de forecasting.

Campo Tipo Observação
meter FK → Meter
cycle_start DateField Início do ciclo de faturamento
cycle_end DateField Fim do ciclo de faturamento
estimated_kwh DecimalField Consumo previsto no ciclo
estimated_cost DecimalField Custo previsto (R$)
confidence FloatField Intervalo de confiança (0–1)
generated_at DateTimeField Momento em que a previsão foi gerada

Appliance (analytics)

Aparelho identificado pelo NILM para um medidor.

Campo Tipo Observação
meter FK → Meter
name CharField Nome do aparelho (ex: Chuveiro)
typical_power FloatField Potência de referência em W — vem do valor digitado/sugerido pela IA ao identificar, ou de uma média móvel com delta_w do evento quando não informada
brand CharField, opcional Marca, usada como contexto na sugestão de potência via IA
model CharField, opcional Modelo, idem

Excluir um Appliance (delete_appliance, tela "Consumo por Aparelho") apaga também seus LoadDisaggregation em cascata e devolve os PowerStepEvent ligados a ele para status='pending' (em vez de ficarem órfãos), para que possam ser identificados novamente.


PowerStepEvent (analytics)

Um degrau de potência detectado em TelemetryReading.active_power. Registrado em todo disaggregate(), classificado ou não — é o que alimenta tanto a composição de consumo (LoadDisaggregation) quanto a fila de calibração manual do usuário.

Campo Tipo Observação
meter FK → Meter
appliance FK → Appliance, null=True None enquanto o evento está pendente
delta_w FloatField Magnitude do salto de potência (W)
delta_a FloatField, null=True Magnitude do salto de corrente (A), calculado a partir do current_rms das mesmas leituras; None em eventos criados antes desse campo existir (ver backfill_delta_a abaixo)
duration_s FloatField, null=True Tempo até a potência retornar perto da baseline anterior; None quando não detectado (ex: degrau de queda, não de subida)
hour_of_day PositiveSmallIntegerField Hora local (0–23) em que o evento ocorreu
measured_at DateTimeField Timestamp do degrau
status CharField pending (sem classificação), auto (classificado por faixa fixa ou classificador), confirmed (atribuído manualmente pelo usuário)
suggested_appliance_name CharField, opcional Palpite do classificador para pré-preencher a identificação manual — nunca aplicado automaticamente ao campo appliance

disaggregate() é idempotente por instante: ao reanalisar o mesmo período, eventos já existentes (meter + measured_at) são reaproveitados em vez de reclassificados/duplicados — isso preserva atribuições manuais já confirmadas entre reanálises.

Limite da fila de pendentes: _enforce_pending_cap mantém só os 30 eventos pending mais recentes por medidor — ao ultrapassar o limite, os mais antigos são apagados automaticamente (chamado ao fim de disaggregate() e após delete_appliance). Eventos auto/confirmed nunca são afetados por esse limite, já que alimentam a classificação (ver abaixo); só a fila de não identificados é podada.

Descarte manual: discard_event apaga um único evento pendente (botão "Descartar" na UI); discard_all_pending_events apaga todos de uma vez para o medidor selecionado (botão "Apagar todos os pendentes").

Algoritmo de classificação (_classify_event, em analytics/services.py)

Três camadas, nesta ordem — da mais específica (dados reais da casa) para a mais genérica:

  1. Classificador por medidor — se houver ≥ 6 PowerStepEvent rotulados (status em auto/confirmed, com appliance definido) cobrindo ≥ 2 aparelhos distintos, treina um sklearn.tree.DecisionTreeClassifier sob demanda com [delta_w, duration_s, hour_of_day] → nome do aparelho. Só aceita a predição se predict_proba máximo ≥ 0.6.
  2. Correspondência por histórico do próprio medidor (_match_known_appliance) — compara o degrau novo contra todo o histórico de eventos auto/confirmed já registrados (não uma média única), dentro de tolerância de ±15%. Permite que um aparelho com múltiplos níveis de consumo legítimos (geladeira com compressor cíclico, micro-ondas usado em potências diferentes, air fryer com termostato ciclando) seja reconhecido em qualquer um desses níveis, não só no mais recente/médio.
  3. Faixas fixas (KNOWN_SIGNATURES) — fallback global, só quando exatamente uma faixa bate (degraus ambíguos entre faixas sobrepostas, ex: 800–4000W de Ar-Condicionado vs. 1000–3500W de Forno Elétrico, ficam pendentes em vez de adivinhar): Chuveiro Elétrico (3000–8000W), Ar-Condicionado (800–4000W), Forno Elétrico (1000–3500W), Geladeira (100–400W).

Se nenhuma camada classificar, o evento fica status='pending' e aparece na tela de Desagregação de Cargas para identificação manual (assign_appliance_to_event, view AssignApplianceView). O formulário de identificação aceita marca/modelo opcionais e um botão "Sugerir com IA" (suggest_appliance_power, mesmo LLM do assistente) que pré-preenche a potência — sempre editável, nunca aplicada sem confirmação do usuário. A atribuição manual marca o evento como confirmed e vira dado de treino para as camadas 1 e 2 nas próximas execuções.

Backfill de dados antigos

python manage.py backfill_delta_a — recalcula delta_a para PowerStepEvent criados antes desse campo existir, buscando a leitura de telemetria no instante exato do evento e a leitura imediatamente anterior. Não precisa rodar de novo para eventos criados depois (já vêm com delta_a preenchido).


LoadDisaggregation (analytics)

Consumo estimado de um aparelho em um período (resultado do NILM). Atualizado via update_or_create por (appliance, period_start, period_end) — reanalisar o mesmo período atualiza o registro existente em vez de duplicá-lo.

Campo Tipo Observação
appliance FK → Appliance
estimated_kwh FloatField Consumo estimado (kWh)
estimated_cost FloatField Custo estimado (R$)
period_start DateField Início do período analisado
period_end DateField Fim do período analisado

UsagePreference (analytics)

Preferência de horário de uso cadastrada pelo usuário para um aparelho (ou rótulo livre), usada para refinar as recomendações de usage-suggestions.

Campo Tipo Observação
owner FK → User
meter FK → Meter
label CharField Nome livre da preferência
appliance FK → Appliance, null=True Aparelho associado; SET_NULL se o aparelho for excluído
preferred_period_type CharField Posto horário preferido (peak, intermediate, off_peak — mesmos choices de TariffTimeSlot)
is_active BooleanField Padrão: True

AssistantMessage (assistant)

Histórico de mensagens trocadas com o assistente — via WhatsApp ou via chat web.

Campo Tipo Observação
user FK → User
direction CharField 'inbound' (usuário) ou 'outbound' (sistema)
content TextField Texto da mensagem
phone_number CharField Número do remetente/destinatário (apenas mensagens WhatsApp)
source CharField 'whatsapp' ou 'web' (padrão: 'whatsapp')
is_proactive_alert BooleanField True quando o sistema enviou sem solicitação prévia

O campo source separa o histórico de WhatsApp do chat web na view de histórico.


Relacionamentos

User ──< Meter ──< TelemetryReading
                └─< DeviceStatusEvent
                └─< OutageEvent
                └─< BillForecast
                └─< Appliance ──< LoadDisaggregation
                └─< PowerStepEvent >── Appliance  (null até ser classificado/confirmado)
                └─< UsagePreference >── Appliance  (null se aparelho excluído)
                └─< BillUpload
User ──< Tariff ──< TariffTimeSlot  (só quando is_white=True)
User ──< BillUpload
User ──< UsagePreference
User ──< AssistantMessage  (source: whatsapp | web)
User ──1 NotificationPreference