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:
@@ -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());
|
||||
Reference in New Issue
Block a user