feat(historico): deteccoes com foto, sensor PIR na placa e tela apagando no sono

- historico.py: eventos de PIR (gpiomon no pino 11, gpiochip1 linha 70),
  movimento e som do celular em SQLite; ate 200 eventos e 100 fotos;
  cada sensor e a foto ligam/desligam; intervalo minimo por tipo
- PIR acorda o Carinha quando ele esta dormindo
- APK 0.8.3: o app consulta o pedido de foto e envia uma foto da camera
  frontal (quadro da camera de movimento ou do video ao vivo)
- tela apaga depois de X minutos dormindo (preto total + brilho minimo no
  AMOLED); acende com som, movimento, toque, comando ou PIR
- painel: tela Historico (sensores, fotos, filtros, apagar), atalho em Ao
  vivo e no Ctrl+K, ajustes "Apagar a tela quando dormir"
- testes do historico (servico e rotas HTTP)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-06 03:40:13 -03:00
co-authored by Claude Opus 5.5
parent 98c9e8803e
commit 788215b49e
20 changed files with 934 additions and 16 deletions
+84
View File
@@ -0,0 +1,84 @@
/** Histórico de detecções (/historico/*): recarrega ao abrir e quando o servidor avisa por SSE. */
import { lerToken, pedir, postar } from './api';
import { avisos } from './avisos.svelte';
import { ouvir } from './estado.svelte';
export interface EventoHistorico {
id: number;
horario: number;
tipo: 'pir' | 'movimento' | 'som';
evento: string;
nivel: number | null;
foto: boolean;
}
export interface StatusHistorico {
config: { pir: boolean; movimento: boolean; som: boolean; foto: boolean; intervalo: number };
pir: { disponivel: boolean; erro: string; linha: string; ultimo: number | null };
eventos: EventoHistorico[];
total: number;
max_eventos: number;
max_fotos: number;
}
export const TIPOS_EVENTO: Record<string, { nome: string; emoji: string; cor: string }> = {
pir: { nome: 'Presença (PIR)', emoji: '🚶', cor: '#34c759' },
movimento: { nome: 'Movimento na câmera', emoji: '👀', cor: '#0a84ff' },
som: { nome: 'Som alto', emoji: '🔊', cor: '#ff9500' },
};
/** URL da foto com o token na query (a tag <img> não manda cabeçalho). */
export function urlFoto(id: number) {
const t = lerToken();
return `/historico/foto/evento-${id}.jpg` + (t ? '?t=' + encodeURIComponent(t) : '');
}
class Historico {
status = $state<StatusHistorico | null>(null);
private espera = 0;
async carregar() {
try {
this.status = await pedir<StatusHistorico>('/historico/status');
} catch (e) {
avisos.erro(e);
}
}
async configurar(parcial: Partial<StatusHistorico['config']>) {
const antes = this.status ? { ...this.status.config } : null;
if (this.status) this.status = { ...this.status, config: { ...this.status.config, ...parcial } };
try {
this.status = await postar<StatusHistorico>('/historico/config', parcial);
} catch (e) {
if (this.status && antes) this.status = { ...this.status, config: antes };
avisos.erro(e);
}
}
async apagar(id: number) {
try {
await postar('/historico/apagar', { id });
} catch (e) {
avisos.erro(e);
}
}
async limpar() {
try {
await postar('/historico/limpar');
avisos.ok('Histórico apagado.');
} catch (e) {
avisos.erro(e);
}
}
/** Vários avisos seguidos (evento + foto) viram uma recarga só. */
agendar() {
clearTimeout(this.espera);
this.espera = window.setTimeout(() => this.carregar(), 300);
}
}
export const historico = new Historico();
ouvir('historico', () => historico.status && historico.agendar());