Skip to content

Latest commit

 

History

History
522 lines (408 loc) · 23 KB

File metadata and controls

522 lines (408 loc) · 23 KB

API POST — referência compacta

Estado: Living · Base: bc31410 · Conferido em 2026-09-19 contra a fonte E contra o aparelho (rig 192.168.3.24): cada código e corpo de resposta citado aqui foi obtido de uma chamada real, não de leitura de código — incluindo a matriz de permissões, exercida com quatro contas de máscaras diferentes.

Todas as 18 rotas POST do firmware, o que cada uma recebe e o que devolve. As permissões são as de AUTHORIZATION.md, que é a fonte normativa — se as duas discordarem, aquela está certa e esta precisa de conserto.


O básico

Sessão por cookie. Tudo exceto /api/login, /api/login_chpass e /api/force_chpass exige SIMUTSESS. Sem sessão: 401. Com sessão mas sem o bit: 403.

# 1. nonce                          2. login (senha em SHA-256 hex do texto puro)
curl -c j -s http://IP/api/login_init          # {"nonce":"…","locked":false,"lockSec":0}
curl -b j -c j -s http://IP/api/login \
     -d "user=admin&pass=$(printf %s 'senha' | sha256sum | cut -d' ' -f1)&nonce=NONCE"
# 3. daqui em diante: curl -b j …

Três formatos de corpo, conforme a rota:

forma rotas
application/x-www-form-urlencoded (campos soltos) login, chpass, commit_all, action, delete, mkdir, history_rebind, save_sys
JSON cru no corpo (lido como plain) calib, set_time, tls
multipart/form-data upload, restore

Lockout: o login tem backoff exponencial por IP. GET /api/login_init devolve locked/lockSec — consulte antes de insistir.

Senha pendente de troca (admin reset) faz commit_all e action?op=reboot responderem 409 com {"next":"/api/force_chpass"}.

Calibração de toque em curso faz as rotas que mexem em config responderem 503.


Quem pode o quê

Dez bits, e o aparelho testa o bit, não um "nível" — não há hierarquia implícita: um usuário com PERM_FILE_DELETE e nada mais apaga arquivos sem conseguir abrir o painel.

bit valor abre
PERM_DASHBOARD 0x0001 painel e status
PERM_HISTORY 0x0002 histórico e exportação
PERM_LOGS 0x0004 leitura do log · metade de clear_logs
PERM_SYS_CONFIG 0x0008 a maior parte da configuração (ver tabela)
PERM_NET_CONFIG 0x0010 seção net do commit_all
PERM_FILE_READ 0x0020 listar arquivos · restore?op=validate
PERM_FILE_UPLOAD 0x0040 upload · mkdir
PERM_FILE_DELETE 0x0080 delete
PERM_USER_MGR 0x0100 seção users · estado de segurança · item Usuários do painel
PERM_CALIB 0x0200 calib
PERM_ALARM_LIMITS (novo) 0x0400 painel: editar limites de alarme de um sensor
PERM_ALARM_BLOCK (novo) 0x0800 painel: ligar/desligar os alarmes de um sensor (e o "Desativar" do pop-up de alarme)
PERM_MAINT (novo) 0x1000 painel: abrir/encerrar a janela de manutenção de um sensor

PERM_ALL_BITS = 0x1FFF (os treze) · PERM_FULL_ADMIN = 0xFFFF.

Os três bits novos são do painel. O painel identifica quem está na frente dele pelo PIN (4–8 dígitos, único por conta) e testa o bit daquela conta antes de cada ação. Na web nada mudou: a seção alarms continua exigindo PERM_SYS_CONFIG, porque é o que a página /alarms precisa para renderizar. Um usuário criado pelo painel nasce só com bits do painel — não entra na web até um admin lhe dar um bit de página e resetar a senha.

Medido no rig (19/09, pico_w_test): pjoao com 0x1C00 e PIN edita limites, bloqueia e abre manutenção; pmaria com 0x1000 vê a linha "Limites" apagada com cadeado e só consegue manutenção; nenhum dos dois aparece no menu Usuários, que exige PERM_USER_MGR. Cada ação sai no log com ctx = usuário×100 + slot (pjoao = conta 4, sensor 0 → 400) e na 2ª linha de telemetria com "user".

O teto que separa os dois

Quatro rotas exigem igualdade exata com PERM_FULL_ADMIN (0xFFFF), e a página de usuários não consegue conceder mais que 0x03FF. Logo:

Nenhuma conta criada pela web — nem marcando todas as caixas — baixa um backup completo nem grava firmware. Isso fica com o users[0], o admin de fábrica. É fronteira deliberada, não acidente de atribuição de bits.

As quatro: GET /api/backup · POST /api/restore?op=apply · POST /api/restore?op=stage · POST /api/ota/apply. (POST /api/tls também.)

O motivo de restore?op=apply estar aqui: um .bkp forjado pode nomear /config/system.bin como destino — o .bkp é o dump do FS inteiro.

Medido no aparelho (conta 0x03FF, todas as dez caixas marcadas):

commit_all alarms  200      GET  /api/backup     403
commit_all net     200      POST /api/ota/apply  403
commit_all users   200

A conta passa em toda seção do commit_all e ainda assim não chega nas duas.

Perfis nomeados (CLI user perm <nome> <papel>)

papel máscara o que alcança
admin / full 0xFFFF tudo, inclusive backup, OTA e TLS
operator / operador 0x0207 painel, histórico, log, calibração
viewer / leitor 0x0003 painel e histórico, só leitura
none / nenhum 0x0000 nada — a conta existe e não entra
0xMASCARA livre qualquer combinação dos dez bits

Padrões de criação: user add pelo CLI dá 0x0203 (painel + histórico + calibração). Pela web, perms é explícito e ausente = 0 — uma conta criada sem marcar caixa nenhuma não entra em lugar nenhum. Novo: pelo painel (Usuários → NOVO) a conta nasce só com os bits do painel escolhidos nas três linhas, uma senha web aleatória que ninguém vê e mustChangePassword — é uma conta do painel até um admin dar bits de página e resetar a senha.

PIN do painel: user pin <nome> <4-8 dígitos|off> no CLI; "pin" na seção users da web (abaixo); ou o próprio usuário no painel (item "Alterar Senha", que hoje é o PIN). Único por conta — o painel identifica pelo PIN (não há campo de usuário): user pin pmaria 5678 respondeu ERRO: PIN ja pertence a pjoao. Teto de contas: 32 (era 5 até a config v23).

O que cada bit destrava, rota a rota

rota POST bit(s)
/api/login /api/login_chpass — (pré-auth, sob lockout por IP)
/api/force_chpass autenticado e com troca pendente
/api/commit_all — entrada qualquer bit que alguma seção use
↳ seção sys slots calib alarms SYS_CONFIG
↳ seção net NET_CONFIG
↳ seção users USER_MGR
/api/action (todos os op) SYS_CONFIG — medido: 0x0008 → 200, viewer → 403
/api/save_sys /api/set_time /api/reset_touch_cal /api/history_rebind /api/touch SYS_CONFIG
/api/clear_logs LOGS e SYS_CONFIG — os dois; SYS_CONFIG sozinho leva 403
/api/calib CALIB
/api/mkdir /api/upload FILE_UPLOAD
/api/delete FILE_DELETE
/api/restore?op=validate FILE_READ
/api/restore?op=apply ?op=stage · /api/ota/apply · /api/tls == 0xFFFF

commit_all é autorizado por seção, não só na entrada. A entrada só prova que há sessão com algum bit que a rota usa; cada seção é conferida depois. E a recusa vale para o payload inteiro — nada é aplicado pela metade. Medido:

conta 0x0100 (só USER_MGR)      {"users":…}              200
                                {"users":…,"net":…}      403   ← o payload todo
                                {"alarms":…}             403
conta 0x0008 (só SYS_CONFIG)    {"alarms":…}             200
                                {"net":…}                403
                                {"net":…} com _dry=1     403
conta 0x0003 (viewer)           {"alarms":…}             403

_dry=1 exige os mesmos bits que a gravação de verdade. Validar não é mais barato em permissão do que aplicar.

Atenção: o commit_all recusa qualquer perms acima de 0x03FF na seção users — escalonar para 0xFFFF por payload não funciona.


Autenticação

POST /api/login — pré-auth

user, pass (SHA-256 hex), nonce (de GET /api/login_init). → 302 + cookie SIMUTSESS, ou 401/429.

POST /api/login_chpass — troca forçada no 1º login — pré-auth

user, oldpass, newpass.

POST /api/force_chpass — autenticado e com troca pendente

p1, p2 (as duas iguais).


Configuração

POST /api/commit_all — a rota principal · PERM_SYS_CONFIG + por seção

Um campo só: _payload = JSON com uma ou mais seções. Teto de 6144 B.

curl -b j -X POST http://IP/api/commit_all \
     --data-urlencode '_payload={"alarms":{"sensors":[{"idx":0,"tmax":41.5}]}}'

Seções e a permissão de cada uma:

seção permissão conteúdo
sys PERM_SYS_CONFIG nome, fuso, log, telemetria, MQTT, 2ª linha, syslog, CORS
slots PERM_SYS_CONFIG provisionamento de sensor
calib PERM_SYS_CONFIG calibração
alarms PERM_SYS_CONFIG limites, liga/desliga, manutenção
net PERM_NET_CONFIG Wi-Fi, IP, porta web
users PERM_USER_MGR contas

Três modos, além do commit normal

campo o que faz
— grava e aplica; reinicia só pelo que precisa
_dry=1 roda todo portão e parser sobre uma cópia, não muda nada e devolve a classificação
_nosave=1 aplica na RAM e não grava: um reinício desfaz. Recusa com 409 se a mudança exigir reinício — e recusa sem ter tocado em nada
_reboot=1 grava e reinicia mesmo sem precisar (reboot_for:["requested"])

_dry e _nosave aceitam sys, net e alarms — as três seções cujos parsers escrevem só em cfg, portanto ensaiáveis numa cópia. As outras respondem 400 {"error":"accepts sys, net and alarms only"}: users gera senhas, slots e calib mexem em arquivos. Ambos indisponíveis no Air.

Quem decide se precisa reiniciar é o aparelho, por comparação com a configuração corrente (src/ConfigApply.h) — um campo reenviado igual ao valor atual não é mudança. Um cliente que reimplemente essa regra vai discordar do aparelho no dia em que um campo mudar de classe.

A resposta diz se reiniciou, e por quê

{"status":"ok","reboot":false,"applied":["alarms","maint"]}
{"status":"ok","reboot":true,"reboot_for":["identity"],"newPort":8080}
{"status":"ok","reboot":false,"applied":[],"rejected":["t_port"]}
{"status":"ok","reboot":false,"saved":false,"applied":["alarms"]}
{"status":"dry","reboot":false,"applied":["alarms"]}
{"status":"dry","reboot":true,"applied":["time"],"reboot_for":["time"]}
  • applied — as classes aplicadas ao vivo, sem reiniciar.
  • reboot_for — as classes que forçaram o reinício, para o próximo pedido evitá-las. Com _reboot=1 sobre uma mudança que não exigia, vem ["requested"].
  • rejected — campos recusados (fora de faixa, inválidos) que mantiveram o valor anterior.
  • saved — só aparece com _nosave=1, e vale sempre false: está na RAM, não na flash.
  • creds — senhas de uso único, quando a seção users criou ou resetou conta. Só aqui.

O que aplica ao vivo — alarmes · manutenção · 2ª linha de alarmes · telemetria pelo lado HTTP · tema e idioma.

O que ainda reinicia — rede · nome do aparelho · contas · provisionamento de slot · cadência/resolução de sensor · MQTT e TLS da telemetria · fuso/NTP · log · PIN do display · política de PIN · porta web e overlays.

Atenção: um campo que ninguém classificou força reinício por segurança (unclassified).

Chaves de sys

name tz log res s_int h_int cors ntp_enabled · política de PIN do painel (v25) pin_min pin_kb pin_alpha · telemetria t_srv t_port t_path t_key t_int t_bat t_mode t_glob t_line t_sep t_sec t_transport · MQTT m_topic m_user m_pass m_qos m_retain m_cid m_ka m_had · 2ª linha (alarmes) a_en a_mode a_qmax a_path a_glob a_line a_sep · syslog slog_en slog_srv slog_port slog_lvl

Atenção: a_line/a_glob/a_sep ficam em sys, não em alarms. Atenção: t_int é lote mínimo em registros, não milissegundos (config v22+). 0 desliga. Atenção: m_qos só aceita 0 — o transporte não entrega QoS 1/2. Atenção: pin_min/pin_kb/pin_alpha viajam como conjunto: os três se restringem (o teclado limita o comprimento, o alfabeto exclui um teclado), então uma combinação impossível recusa o campo pin_min inteiro em vez de aplicar metade. pin_kb é 1/2/3 glifos por tecla e pin_alpha 0 (0-9) ou 1 (0-9A-Z); comprimento máximo 16/12/8 conforme o teclado. Apertar a política marca toda conta com PIN para trocá-lo — o aparelho guarda só o digest e não tem como saber se um PIN antigo ainda cabe.

Chaves de net

ssid pass use_dhcp ip mask gw dns dns1 dns2 dns_auto ntp_server web_port web_ka Endereço inválido vai para rejected, não é descartado em silêncio.

slots — provisionamento

{"slots":{"s":[{"i":0,"a":true,"t":2,"p":[2,255,255,255],
                "hwId":"DHT0","name":"Sala",
                "lim":{"temp":[-10,50],"hum":[0,100]},"al":true}]}}

Mande só os slots editados — e só os campos editados: o parser escreve apenas o que veio, então {"i":0,"name":"Sala"} não apaga tipo, pinos nem limites.

tmin/tmax/hmin/hmax não valem aqui (21/09/2026). Este exemplo os trazia e o parser da seção slots nunca os leu: o commit responde 200 com applied e rejected vazios e nada muda. Limites vão em lim, por canal, ou pela seção alarms.

users — contas

{"users":{"actions":[{"type":"add","name":"op","perms":511,"pin":"2222"},
                     {"type":"del","id":3},
                     {"type":"reset","id":5},
                     {"type":"pin","id":5,"pin":"123456"}]}}

add e reset devolvem a senha em creds — entregue só nessa resposta.

Novo: pin é opcional no add e existe como ação própria ("" remove; id 0 = o admin, é assim que se define o PIN do admin pela web). Um PIN malformado ou repetido rejeita o campo, não a conta: medido, {"type":"pin","id":0,"pin":"12"} → 200 {"status":"ok","reboot":false,"applied":[],"rejected":["users.pin"]}; e {"type":"add","name":"web1","perms":4096,"pin":"2222"} → 200 {"status":"ok","reboot":true,"reboot_for":["users"],"creds":[{"u":"web1","p":"…"}]} — a conta web1 entrou no painel com 2222 depois do reboot (log 308 ctx=6).

GET /api/users agora diz "pin":true|false por conta (nunca o PIN): [{"id":0,"name":"admin","perms":65535,"pin":true},…].


Alarmes e manutenção

POST /api/commit_all seção alarms · PERM_SYS_CONFIG

{"alarms":{"sensors":[
  {"idx":0, "active":true,
   "tmin":-10, "tmax":45,
   "temp":[-10,45], "hum":[20,80], "press":[900,1100], "lux":[0,5000],
   "maint":7200}
]}}
campo efeito
idx slot 0..15, obrigatório; slot inativo é ignorado
active liga/desliga o alarme de limite deste slot
tmin tmax hmin hmax nomes antigos de temperatura e umidade
"<canal>":[min,max] forma por canal (temp hum press lux) — vence sobre a anterior
maint (novo) segundos a partir de agora; 0 fecha a janela

Tudo isso aplica ao vivo. Resposta: {"reboot":false,"applied":["alarms","maint"]}.

Limite fora da faixa plausível do canal → 400 {"error":"Alarm limit outside channel range"}, e nenhuma seção é aplicada — o commit é atômico. Banda invertida (min ≥ max) é corrigida automaticamente.

O modo manutenção

maint em segundos, não epoch: "este sensor sai por duas horas" não deve depender de os dois relógios concordarem. Teto 30 dias — acima disso é cortado e aparece em rejected.

Enquanto a janela está aberta, aquele slot:

  • não gera registro de limite nem de falha na 2ª linha;
  • não pisca nem apita no painel — desconectar o sensor é a manutenção;
  • gera exatamente dois registros: ao entrar e ao sair.

A janela persiste em flash e o aparelho sai dela sozinho no vencimento.

# põe o slot 3 em manutenção por 2 h
curl -b j -X POST http://IP/api/commit_all \
     --data-urlencode '_payload={"alarms":{"sensors":[{"idx":3,"maint":7200}]}}'
# tira
curl -b j -X POST http://IP/api/commit_all \
     --data-urlencode '_payload={"alarms":{"sensors":[{"idx":3,"maint":0}]}}'

GET /api/alarms devolve "maint":<segundos restantes> por slot (0 = fora).

No payload da 2ª linha

Terceiro domínio, separado de alarm e err, para um servidor que casa por campo nunca confundir manutenção com falha. Desde a config v24 os códigos são maint_on/maint_off, o registro de entrada diz até quando (until, epoch, resolução de 1 min) e todo registro causado por alguém diz quem (user) — recebidos do rig em 19/09, pelo painel, conta pjoao:

{"ts":1789834707,"id":"tSTM0009","alarm":"alarm_lim","lo":2.70,"hi":24.00,"user":"pjoao","seq":1}
{"ts":1789834722,"id":"tSTM0009","alarm":"alarm_off","user":"pjoao","seq":2}
{"ts":1789834737,"id":"tSTM0009","alarm":"alarm_on","user":"pjoao","seq":3}
{"ts":1789834751,"id":"tSTM0009","maint":"maint_on","until":1789835051,"user":"pjoao","seq":4}
{"ts":1789834767,"id":"tSTM0009","maint":"maint_off","user":"pjoao","seq":5}
código domínio quando campos extras
alarm_lim alarm limites editados lo, hi (decimais do canal), user
alarm_off / alarm_on alarm alarmes do sensor desligados / religados user
maint_on maint janela aberta (painel ou web) until, user
maint_off maint janela fechada por comando ou por prazo user (vazio no prazo)

Tokens novos: {LO} {HI} {UNTIL} {USER} (aliases minúsculos), todos na forma composta — a chave some quando não se aplica. Já estão no template default; um template que ainda era exatamente o default antigo (v21 ou v23) foi trocado pelo novo na migração; um template editado é do operador e fica como está. No modo CSV a linha ganhou quatro colunas no fim: seq;ts;id;v;user;lo;hi;until (vazias quando não se aplicam).

Um template custom escrito antes da v23/v24 não tem {MAINT} nem os tokens novos. Acrescente-os, ou pergunte por GET /api/alarms.


Ações pontuais

POST /api/action?op=… · PERM_SYS_CONFIG

op efeito
reboot reinicia sem inventar um commit para isso
tel_sync força um ciclo de telemetria agora
tel_reset rearma o cursor de telemetria (rebobina o pendente)
sensor_scan inicia varredura de sensores → 202 {"started":true}
scan_results resultado da varredura; {"scanning":true} enquanto corre
sensor_wipe &slot=N — apaga o slot e carimba provisionEpoch = agora
sensor_accept &slot=N — aceita o hardware encontrado no slot

op desconhecido → 400 {"error":"op"}; slot ausente ou fora de 0..15 → 400 {"error":"slot"}. O op é validado antes do slot, para um op errado não apontar o chamador para o parâmetro errado. tel_sync → {"ok":true}.

POST /api/set_time · PERM_SYS_CONFIG

JSON cru {"epoch":1789797592} → {"ok":true,"now":1789797592}. Aplica na hora, sem reiniciar. Com NTP ligado, a próxima sincronização sobrescreve.

POST /api/save_sys · PERM_SYS_CONFIG

theme=<n> → {"status":"ok"}. Troca o tema na hora, sem passar pelo commit.

POST /api/reset_touch_cal · PERM_SYS_CONFIG

Sem corpo. Invalida a calibração de toque.

POST /api/history_rebind · PERM_SYS_CONFIG

force=1 opcional. Reancora o histórico ao esquema atual. Reinicia ao fim.

POST /api/clear_logs · PERM_LOGS e PERM_SYS_CONFIG

Sem corpo. Atenção: não zera o que show system log devolve (ele costura o rotacionado) — só valem deltas entre duas leituras.

POST /api/touch · PERM_SYS_CONFIG

x=0..319, y=0..239 — um toque no painel, em coordenadas do painel. Fora da faixa é 400, não clamp. Não espera o painel repintar antes de responder: o UiEvent é consumido pelo mesmo core que atende a requisição. Quem chama é que espera.


Calibração

POST /api/calib · PERM_CALIB

JSON cru, até ~8 KB. Exige NTP sincronizado (503 se não) e tem limite de taxa (429).

{"sensors":[{"slot":0, "name":"Sala",
             "refs":{"temp":25.0,"hum":60.0,"press":1013.2},
             "end":false}]}

slot (ou gpio) identifica; refs por canal é a forma nova — refTemp/refHum/refPress continuam aceitos.


Arquivos

POST /api/upload · PERM_FILE_UPLOAD

multipart/form-data, campo de arquivo; uploadDir opcional define o destino.

curl -b j -F "file=@pack.lng" -F "uploadDir=/lang" http://IP/api/upload

Atenção: pacote .lng só passa a valer no boot seguinte.

POST /api/mkdir · PERM_FILE_UPLOAD

dir=/caminho. Ausente → 400 Missing dir.

POST /api/delete · PERM_FILE_DELETE

file=/caminho — o parâmetro é file, não path. Ausente → 400 Bad Request.


OTA e restauração

POST /api/restore?op=… multipart

op permissão efeito
validate PERM_FILE_READ confere o .bkp sem gravar
apply == PERM_FULL_ADMIN sobrescreve o LittleFS inteiro
stage == PERM_FULL_ADMIN prepara uma imagem de firmware

A permissão é conferida no primeiro byte do fluxo, não só no fim.

POST /api/ota/apply · == PERM_FULL_ADMIN

test=1 para um ensaio sem aplicar. O apply zera o /history e o LittleFS — faça backup antes. Atenção: transferência grande na porta 80 cai (RST do roteador): use :8080.

POST /api/tls · == PERM_FULL_ADMIN · só onde há HTTPS

Corpo cru com os dois blocos PEM concatenados (certificado + chave privada), até 8 KB. Os dois precisam casar antes de qualquer arquivo ser escrito.

curl -b j -X POST --data-binary @<(cat cert.pem key.pem) http://IP/api/tls

Chave com passphrase é recusada com a instrução para decifrar. Atenção: br_rsa_compute_pubexp exige p ≡ 3 mod 4 e recusa ~metade das chaves RSA — P-521 (EC) funciona sempre.


Códigos de resposta

200 feito (veja reboot/applied/rejected no corpo)
202 aceito e rodando (varredura de sensores)
302 login OK
400 corpo/parâmetro inválido — nada foi aplicado
401 sem sessão
403 sessão sem o bit necessário
409 senha pendente de troca → /api/force_chpass
413 corpo acima do teto
429 limite de taxa (login, calib, tls)
500 falha ao gravar
503 ocupado (calibração de toque, varredura, sem NTP, sem memória)