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, é oviewing. Toda query de dados do usuário (web, API, assistente) usaowner=<algo>.effective_owner. Um usuário pode ter múltiplos visualizadores (user.viewers.all()). VerPRD.mdseçã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 detelemetry: 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_billedetaxes_percent_billedda fatura, ativando-a como tarifa corrente. O usuário não precisa cadastrar tarifa manualmente. Para tarifa branca,tariffs/management/commands/sync_white_tariff.pysincroniza 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_togetherpor(tariff, period_type)sozinho: algumas distribuidoras (ex.: CELESC) dividem o posto intermediário em dois intervalos, um de cada lado da ponta. Ounique_togetherreal é(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 emTariffacima. Verdocs/lgpd.mdpara 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:
- Classificador por medidor — se houver ≥ 6
PowerStepEventrotulados (statusemauto/confirmed, comappliancedefinido) cobrindo ≥ 2 aparelhos distintos, treina umsklearn.tree.DecisionTreeClassifiersob demanda com[delta_w, duration_s, hour_of_day]→ nome do aparelho. Só aceita a predição sepredict_probamáximo ≥ 0.6. - Correspondência por histórico do próprio medidor (
_match_known_appliance) — compara o degrau novo contra todo o histórico de eventosauto/confirmedjá 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. - 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
sourcesepara 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