Arquitetura Contrato de API - ES2-UFPI/TermoLOG GitHub Wiki

Contrato de API

Esta página define como o backend conversa com os outros dois lados do sistema, sempre por HTTPS com uma API REST no FastAPI:

  1. Gateway (nó central) ↔ backend: envio das leituras e retorno de configuração e comandos.
  2. Dashboard ↔ backend: tudo o que o painel web mostra e altera.

O contrato existe para que firmware, backend e frontend possam ser desenvolvidos em paralelo (#28). Qualquer mudança nele deve ser combinada com quem implementa os lados afetados.

Convenções gerais

  • Todo payload é JSON em UTF-8.
  • Datas e horas seguem a ISO 8601 com fuso, por exemplo 2026-10-16T14:30:00-03:00. O backend grava em UTC. O gateway precisa sincronizar o relógio por NTP antes de enviar leituras.
  • Temperatura em °C e umidade em % de umidade relativa, os dois como número decimal.
  • IDs de sala, sensor, gateway, comando, alerta e usuário são inteiros gerados pelo backend.
  • Os erros usam o formato padrão do FastAPI: { "detail": "mensagem" }. Nos erros de validação (422), o detail é a lista de campos inválidos.

1. Gateway ↔ backend

O gateway fica atrás do NAT da filial, sem IP público, então o backend nunca chama o gateway. Toda conversa começa com o gateway enviando um lote, e tudo o que o backend precisa mandar de volta (confirmação, configuração nova e comandos) vai na resposta desse envio.

sequenceDiagram
    participant N as Nó de borda
    participant G as Gateway (nó central)
    participant A as Backend
    N->>G: leitura (LoRa)
    G->>G: guarda na fila
    G->>A: POST /gateways/{id}/leituras (lote)
    A-->>G: 201 (ack + config nova + comandos)
    G->>G: tira o lote da fila e guarda os comandos por sensor
    N->>G: próxima leitura (LoRa)
    G-->>N: ack + comando pendente, na janela de recepção do nó
    N->>N: executa (ex. apita) e volta ao deep sleep
Loading

1.1 Autenticação

O gateway manda em todas as requisições a chave de API gerada no cadastro dele (POST /gateways, US27):

Authorization: Bearer <chave_api>
Código Quando O que o gateway faz
401 { "detail": "credencial inválida" }. Chave ausente, inválida ou revogada. Não reenvia em loop: espera com backoff (ex. 1, 2, 4… até 5 min), mantém a fila e sinaliza o erro (LED ou log).
403 { "detail": "credencial não pertence a este gateway" }. A chave é válida, mas o {id} da URL é de outro gateway. Erro de configuração do firmware: registra no log e mantém a fila.

O backend confere a chave e o {id} antes de ler o corpo da requisição.

POST /gateways/{id}/leituras · Gateway

Registra um lote de leituras que o gateway recebeu dos seus nós (#30).

  • Req:
{
  "lote_id": "3f0c6d1e-8a2b-4c55-9b7e-2d1f0a9c4e11",
  "versao_config": 7,
  "comandos_concluidos": [4],
  "leituras": [
    { "sensor_id": 12, "temperatura": 4.3, "umidade": 61.0, "data_hora": "2026-10-16T14:30:00-03:00" },
    { "sensor_id": 13, "temperatura": 5.1, "umidade": 58.5, "data_hora": "2026-10-16T14:30:02-03:00" }
  ]
}
Campo Tipo Regra
lote_id string (UUID) Gerado pelo gateway, um por lote. Um lote reenviado mantém o mesmo lote_id.
versao_config inteiro Versão da configuração que o gateway já aplicou. Depois de ligar, manda 0.
comandos_concluidos lista de inteiros IDs dos comandos já entregues aos nós desde o último envio (1.3). Pode ser vazia.
leituras lista De 0 a 100 itens. Lista vazia é um heartbeat (1.2).
sensor_id inteiro Sensor cadastrado e ativo (#38).
temperatura número Entre -40 e 80 (limite físico do DHT22).
umidade número Entre 0 e 100.
data_hora string (ISO 8601) Hora da leitura no nó de borda. Não pode estar mais de 5 min no futuro.
  • 201:
{
  "lote_id": "3f0c6d1e-8a2b-4c55-9b7e-2d1f0a9c4e11",
  "aceitas": 2,
  "rejeitadas": [],
  "config": null,
  "comandos": [
    { "id": 5, "sensor_id": 12, "tipo": "apitar", "duracao_s": 10, "expira_em": "2026-10-16T15:30:00-03:00" }
  ]
}
Campo Significado
aceitas Quantas leituras foram gravadas.
rejeitadas[].indice Posição do item no array leituras (começa em 0).
rejeitadas[].sensor_id Sensor do item recusado.
rejeitadas[].motivo sensor_nao_cadastrado, sensor_inativo, valor_fora_do_limite ou data_hora_invalida.
config Configuração completa dos sensores do gateway, só quando versao_config está desatualizada. Se não, null (1.4).
comandos Comandos pendentes para os nós desse gateway. Lista vazia se não houver.
  • 422: lote malformado (JSON inválido, sem lote_id ou com mais de 100 leituras). O gateway descarta o lote e registra o erro no log, porque reenviar não resolve: o problema está no firmware.
  • 401 / 403: ver 1.1.

1.2 Regras do envio

  • Fila: o gateway só tira o lote da fila quando recebe o 201 (#33). Sem resposta (internet fora, timeout de 30 s, erro 5xx), reenvia o mesmo lote com o mesmo lote_id quando a conexão voltar.
  • Idempotência: um lote_id já processado não é gravado de novo. O backend devolve o mesmo resultado da primeira vez. Isso cobre o caso em que o lote chegou, mas a resposta se perdeu no caminho.
  • Validação por item: um item inválido não derruba o lote. Os válidos são gravados e os inválidos voltam em rejeitadas. O gateway tira o lote inteiro da fila, inclusive as rejeitadas, porque reenviar não muda o resultado, e registra as rejeitadas no log.
  • Heartbeat: se a fila ficar vazia por 60 s, o gateway envia um lote com leituras: []. É assim que ele recebe configuração e comandos mesmo sem leituras novas, e é por esse contato que o backend sabe que o gateway está online (GET /gateways).

1.3 Comandos do servidor para o nó

Um comando (por exemplo, apitar o buzzer de um nó) faz dois saltos, e nos dois quem recebe nunca é chamado diretamente:

  1. Backend → gateway: o comando fica pendente no backend até o próximo envio do gateway e vai no campo comandos da resposta. Atraso máximo: o intervalo de envio do gateway (60 s no pior caso, por causa do heartbeat).
  2. Gateway → nó: o nó passa a maior parte do tempo em deep sleep, com o rádio desligado. O gateway guarda o comando numa fila por sensor_id e só o entrega quando aquele nó acorda e envia a próxima leitura. O nó deixa o rádio ouvindo por cerca de 1 s depois de cada envio, e o gateway responde nessa janela com o ack da leitura e o comando. Atraso máximo: o intervalo de leitura do nó.

Depois de entregar o comando ao nó, o gateway informa o id em comandos_concluidos no próximo lote. Até isso acontecer, o backend continua mandando o mesmo comando, e o gateway ignora um id que já tem na fila. Um comando que passa do expira_em sem ser entregue é descartado pelo gateway e pelo backend.

Tipo Campos Efeito no nó
apitar duracao_s Liga o buzzer pelo tempo indicado.

O alarme por leitura fora da faixa não depende de comando: o nó compara cada leitura com a faixa que recebeu na configuração e apita sozinho (US21).

1.4 Configuração

Sempre que um administrador muda a faixa ou a frequência de uma sala, ou cadastra ou desativa um sensor, a versão da configuração dos gateways afetados aumenta. Na próxima resposta a esses gateways, o campo config vem preenchido:

{
  "versao": 8,
  "sensores": [
    {
      "sensor_id": 12,
      "intervalo_leitura_s": 300,
      "faixa": { "temperatura_min": 2.0, "temperatura_max": 8.0, "umidade_min": 40.0, "umidade_max": 70.0 }
    }
  ]
}
  • A configuração vem sempre completa, não só o que mudou. O gateway substitui a anterior e passa a mandar versao_config: 8.
  • Depois de reiniciar, o gateway manda versao_config: 0 e recebe a configuração completa no primeiro envio.
  • O gateway repassa a cada nó, por LoRa e na mesma janela de recepção dos comandos, o intervalo de leitura (US03) e a faixa usada no alarme local (US21).
  • Um sensor que sai da lista foi desativado. O gateway deixa de repassar configuração para ele.

1.5 Exemplo de lote com itens inválidos

{
  "lote_id": "9b1e2c3d-0000-4a1b-8c2d-123456789abc",
  "versao_config": 8,
  "comandos_concluidos": [],
  "leituras": [
    { "sensor_id": 12, "temperatura": 4.3, "umidade": 61.0, "data_hora": "2026-10-16T14:30:00-03:00" },
    { "sensor_id": 99, "temperatura": 4.0, "umidade": 60.0, "data_hora": "2026-10-16T14:30:00-03:00" },
    { "sensor_id": 13, "temperatura": 5.1, "umidade": 140.0, "data_hora": "2026-10-16T14:30:00-03:00" }
  ]
}

Resposta 201:

{
  "lote_id": "9b1e2c3d-0000-4a1b-8c2d-123456789abc",
  "aceitas": 1,
  "rejeitadas": [
    { "indice": 1, "sensor_id": 99, "motivo": "sensor_nao_cadastrado" },
    { "indice": 2, "sensor_id": 13, "motivo": "valor_fora_do_limite" }
  ],
  "config": null,
  "comandos": []
}

2. Dashboard ↔ backend

2.1 Autenticação e perfis

  • Depois do login, o dashboard manda o token em todas as requisições: Authorization: Bearer <token>.
  • A sessão expira após 30 minutos sem requisições (US27). Depois disso qualquer rota responde 401 e o dashboard volta para a tela de login.
  • Perfis (US24): administrador e visualizador. As rotas marcadas como Admin respondem 403 para o visualizador, e a tentativa fica registrada.

Erros comuns a todas as rotas (não se repetem abaixo):

Código Quando
401 { "detail": "não autenticado" }. Sem token, token inválido ou sessão expirada.
403 { "detail": "permissão insuficiente" }. Perfil sem acesso à rota.
404 { "detail": "<recurso> não encontrado" }. ID do path não existe.
422 Erro de validação do FastAPI, com a lista de campos inválidos.

Objeto faixa (usado em salas, status e configuração):

{ "temperatura_min": 2.0, "temperatura_max": 8.0, "umidade_min": 40.0, "umidade_max": 70.0 }

O mínimo tem que ser menor que o máximo. Se não for, a resposta é 422.

2.2 Sessão

POST /auth/login · Público

Inicia a sessão com e-mail e senha (US27).

  • Req: { "email", "senha" }
  • 200: { "token", "expira_em", "usuario": { "id", "nome", "email", "perfil" } }
  • 401: { "detail": "e-mail ou senha inválidos" }. A mensagem é a mesma para e-mail inexistente e senha errada.

POST /auth/logout · Logado

Encerra a sessão atual. O token deixa de valer.

  • 204: sem corpo.

GET /auth/me · Logado

Retorna o usuário da sessão. O dashboard usa para decidir o que mostrar conforme o perfil.

  • 200: { "id", "nome", "email", "perfil" }

2.3 Painel

GET /salas/status · Logado

Situação atual de todas as salas em uma chamada (#45). O dashboard chama essa rota a cada 30 s para atualizar os cards sem recarregar a página (#48).

  • 200:
[
  {
    "sala_id": 1,
    "nome": "Câmara fria 1",
    "faixa": { "temperatura_min": 2.0, "temperatura_max": 8.0, "umidade_min": 40.0, "umidade_max": 70.0 },
    "intervalo_leitura_s": 300,
    "status": "fora_da_faixa",
    "leituras": [
      { "sensor_id": 12, "temperatura": 9.1, "umidade": 61.0, "data_hora": "2026-10-16T14:30:00-03:00" }
    ]
  }
]

status pode ser ok, fora_da_faixa ou sem_leitura. leituras traz a última leitura de cada sensor ativo da sala. Uma sala sem leitura vem com a lista vazia.

2.4 Salas

GET /salas · Logado

Lista as salas.

  • 200: [{ "id", "nome", "descricao", "intervalo_leitura_s", "faixa" }]

GET /salas/{id} · Logado

Detalhe de uma sala com os sensores dela.

  • 200: { "id", "nome", "descricao", "intervalo_leitura_s", "faixa", "sensores": [{ "id", "gateway_id", "ativo", "data_cadastro" }] }

POST /salas · Admin

Cadastra uma sala (#37).

  • Req: { "nome", "descricao"?, "intervalo_leitura_s", "faixa" }
  • 201: a sala criada, no mesmo formato de GET /salas/{id}.

PATCH /salas/{id} · Admin

Altera nome, descrição, frequência de leitura ou faixa da sala (#41, US03). Só os campos enviados mudam.

  • Req: qualquer subconjunto de { "nome", "descricao", "intervalo_leitura_s", "faixa" }
  • 200: a sala atualizada.
  • Se a faixa ou o intervalo mudar, a versão da configuração dos gateways da sala aumenta e eles recebem a nova no próximo envio (1.4).

2.5 Sensores

POST /salas/{id}/sensores · Admin

Cadastra um sensor na sala. O ID é gerado pelo backend e nunca é reaproveitado (#36).

  • Req: { "gateway_id" }
  • 201: { "id", "sala_id", "sala_nome", "gateway_id", "ativo", "data_cadastro" }

POST /sensores/{id}/desativar · Admin

Desativa o sensor. Não existe exclusão: o sensor desativado mantém o ID e o histórico (#36). Chamar de novo num sensor já inativo não muda nada.

  • 200: o sensor com "ativo": false.

POST /sensores/{id}/comandos · Admin

Cria um comando para o nó do sensor, por exemplo apitar o buzzer para localizar o aparelho. O comando segue pelo gateway e chega ao nó no próximo ciclo de leitura dele (1.3).

  • Req: { "tipo": "apitar", "duracao_s": 10 }
  • 202: { "id", "sensor_id", "tipo", "duracao_s", "status": "pendente", "expira_em" }. O 202 indica que o comando foi aceito, mas ainda não chegou ao nó.
  • 409: { "detail": "sensor inativo" }

GET /sensores/{id}/leituras · Logado

Histórico de leituras de um sensor, em ordem cronológica (#39).

  • Query: inicio e fim (ISO 8601, opcionais), pagina (padrão 1), tamanho (padrão 100, máximo 1000).
  • 200: { "sensor_id", "sala_nome", "total", "pagina", "itens": [{ "temperatura", "umidade", "data_hora" }] }

2.6 Gateways

GET /gateways · Admin

Lista os gateways. online é true quando o último envio ou heartbeat (1.2) foi há menos de 3 minutos.

  • 200: [{ "id", "nome", "online", "ultimo_contato" }]

POST /gateways · Admin

Cadastra um gateway e gera a chave de API que ele usa para se autenticar (1.1).

  • Req: { "nome" }
  • 201: { "id", "nome", "chave_api" }. A chave_api aparece só nesta resposta, porque o backend guarda apenas o hash dela. Ela tem que ser gravada no firmware do gateway.

2.7 Alertas

Um alerta passa por pendente → reconhecido → encerrado. Depois de encerrado ele não muda mais (US25).

GET /alertas · Logado

Lista os alertas, com os mais recentes primeiro. Pendentes e reconhecidos aparecem separados no painel filtrando por status (US12).

  • Query: status (pendente, reconhecido ou encerrado, opcional), sala_id (opcional).
  • 200:
[
  {
    "id": 31,
    "sala_id": 1,
    "sala_nome": "Câmara fria 1",
    "sensor_id": 12,
    "tipo": "fora_da_faixa",
    "status": "reconhecido",
    "aberto_em": "2026-10-16T14:30:05-03:00",
    "leitura": { "temperatura": 9.1, "umidade": 61.0, "data_hora": "2026-10-16T14:30:00-03:00" },
    "reconhecido_por": { "id": 3, "nome": "Ana" },
    "reconhecido_em": "2026-10-16T14:35:10-03:00",
    "acao_corretiva": null,
    "encerrado_em": null
  }
]

tipo pode ser fora_da_faixa ou sem_leitura. Num alerta sem_leitura, o campo leitura vem null.

POST /alertas/{id}/reconhecer · Logado

Marca o alerta como visto (US12). Quem reconheceu e quando saem da sessão, nunca do corpo da requisição.

  • 200: o alerta atualizado.
  • 409: { "detail": "alerta não está pendente" }

POST /alertas/{id}/encerrar · Logado

Registra a ação corretiva e encerra o alerta (US26).

  • Req: { "acao_corretiva" } (obrigatório, não pode ser vazio)
  • 200: o alerta encerrado.
  • 409: { "detail": "alerta precisa estar reconhecido" } ou { "detail": "alerta já encerrado" }

2.8 Relatórios

GET /relatorios/leituras · Logado

Relatório de leituras de uma sala num período, para a vigilância sanitária (US10, US15).

  • Query: sala_id, inicio, fim, formato (pdf, xlsx ou csv).
  • 200: o arquivo, com Content-Type do formato e Content-Disposition: attachment.

GET /relatorios/alertas · Logado

Relatório dos alertas e das ações corretivas de uma sala num período (US26).

  • Query e 200: iguais aos de /relatorios/leituras.

Fora deste contrato por enquanto

  • Nenhuma rota altera ou exclui leituras ou alertas encerrados (#54, US25). Isso é proposital.
  • Cadastro de usuários, canais de notificação e plantão (US17, US20, US24) e calibração dos sensores (US18) entram aqui quando forem planejados.
⚠️ **GitHub.com Fallback** ⚠️