Files
imperial-command-center/src/stores/settingsStore.ts
root 58ebd3e239 Fix camera feed freezing and person detection alerts
- Add WebRTC auto-reconnect with exponential backoff when streams
  disconnect or fail, preventing permanent freezes in grid view
- Replace hard-coded Front Porch person alert with generic system
  that monitors all configured personDetectionEntities
- Map Frigate person_occupancy entities to cameras dynamically
- Show correct camera name and feed in alert overlay
- Bump config version to refresh detection entity defaults
2026-02-26 15:33:25 -06:00

260 lines
9.8 KiB
TypeScript

import { create } from 'zustand';
import { persist } from 'zustand/middleware';
// Increment this when default cameras/config changes to force refresh
const CONFIG_VERSION = 9;
export interface ThermostatConfig {
entityId: string;
name: string;
location?: string;
}
export interface LightConfig {
entityId: string;
name: string;
room: string;
}
export interface LockConfig {
entityId: string;
name: string;
}
export interface PersonConfig {
entityId: string;
name: string;
avatarUrl?: string;
}
export interface CameraConfig {
name: string;
displayName: string;
go2rtcStream: string;
frigateCamera?: string;
}
export interface DashboardConfig {
// Selected entities
thermostats: ThermostatConfig[];
lights: LightConfig[];
locks: LockConfig[];
alarm: string | null;
calendar: string | null;
todoList: string | null;
people: PersonConfig[];
packageSensor: string | null;
// Cameras (manual config for now since they come from go2rtc, not HA)
cameras: CameraConfig[];
personDetectionEntities: string[];
// Dashboard settings
go2rtcUrl: string;
frigateUrl: string;
jellyfinUrl: string;
jellyfinApiKey: string | null;
// Setup completed flag
setupCompleted: boolean;
// Config version - used to detect when defaults change
configVersion: number;
}
interface SettingsState {
config: DashboardConfig;
// Actions
setThermostats: (thermostats: ThermostatConfig[]) => void;
setLights: (lights: LightConfig[]) => void;
setLocks: (locks: LockConfig[]) => void;
setAlarm: (alarm: string | null) => void;
setCalendar: (calendar: string | null) => void;
setTodoList: (todoList: string | null) => void;
setPeople: (people: PersonConfig[]) => void;
setPackageSensor: (sensor: string | null) => void;
setCameras: (cameras: CameraConfig[]) => void;
setPersonDetectionEntities: (entities: string[]) => void;
setGo2rtcUrl: (url: string) => void;
setFrigateUrl: (url: string) => void;
setJellyfinUrl: (url: string) => void;
setJellyfinApiKey: (key: string | null) => void;
setSetupCompleted: (completed: boolean) => void;
// Bulk update
updateConfig: (partial: Partial<DashboardConfig>) => void;
resetConfig: () => void;
}
const defaultConfig: DashboardConfig = {
thermostats: [
{ entityId: 'climate.kitchen_side', name: 'Kitchen side' },
{ entityId: 'climate.master_side', name: 'Master side' },
],
lights: [
{ entityId: 'light.back_porch_master', name: 'Back porch master', room: 'Outside' },
{ entityId: 'light.master_light', name: 'Bedroom light', room: 'Master' },
{ entityId: 'light.chris_lamp', name: 'Chris lamp', room: 'Master' },
{ entityId: 'light.front_floods', name: 'Front flood lights', room: 'Outside' },
],
locks: [],
alarm: null,
calendar: 'calendar.family',
todoList: 'todo.shopping_list',
people: [],
packageSensor: 'binary_sensor.package_detected',
cameras: [
// Online cameras
{ name: 'FPE', displayName: 'Front Porch Entry', go2rtcStream: 'FPE', frigateCamera: 'FPE' },
{ name: 'Porch_Downstairs', displayName: 'Porch Downstairs', go2rtcStream: 'Porch_Downstairs', frigateCamera: 'Porch_Downstairs' },
{ name: 'Front_Porch', displayName: 'Front Porch', go2rtcStream: 'Front_Porch', frigateCamera: 'Front_Porch' },
{ name: 'Driveway_door', displayName: 'Driveway Door', go2rtcStream: 'Driveway_door', frigateCamera: 'Driveway_door' },
{ name: 'Street_side', displayName: 'Street Side', go2rtcStream: 'Street_side', frigateCamera: 'Street_side' },
{ name: 'Backyard', displayName: 'Backyard', go2rtcStream: 'Backyard', frigateCamera: 'Backyard' },
{ name: 'House_side', displayName: 'House Side', go2rtcStream: 'House_side', frigateCamera: 'House_side' },
{ name: 'Driveway', displayName: 'Driveway', go2rtcStream: 'Driveway', frigateCamera: 'Driveway' },
{ name: 'WyzePanV3', displayName: 'Wyze Pan V3', go2rtcStream: 'WyzePanV3', frigateCamera: 'WyzePanV3' },
// Thingino cameras
{ name: 'BackDoor', displayName: 'Back Door', go2rtcStream: 'BackDoor', frigateCamera: 'BackDoor' },
{ name: 'Parlor', displayName: 'Parlor', go2rtcStream: 'Parlor', frigateCamera: 'Parlor' },
{ name: 'Livingroom', displayName: 'Living Room', go2rtcStream: 'Livingroom', frigateCamera: 'Livingroom' },
],
personDetectionEntities: [
'binary_sensor.fpe_person_occupancy',
'binary_sensor.porch_downstairs_person_occupancy',
'binary_sensor.front_porch_person_occupancy',
'binary_sensor.driveway_door_person_occupancy',
'binary_sensor.driveway_person_occupancy',
'binary_sensor.backyard_person_occupancy',
'binary_sensor.street_side_person_occupancy',
'binary_sensor.house_side_person_occupancy',
],
go2rtcUrl: 'http://192.168.1.241:1985',
frigateUrl: 'http://192.168.1.241:5000',
jellyfinUrl: 'http://192.168.1.49:8096',
jellyfinApiKey: null,
setupCompleted: false,
configVersion: CONFIG_VERSION,
};
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({
config: { ...defaultConfig },
setThermostats: (thermostats) =>
set((state) => ({ config: { ...state.config, thermostats } })),
setLights: (lights) =>
set((state) => ({ config: { ...state.config, lights } })),
setLocks: (locks) =>
set((state) => ({ config: { ...state.config, locks } })),
setAlarm: (alarm) =>
set((state) => ({ config: { ...state.config, alarm } })),
setCalendar: (calendar) =>
set((state) => ({ config: { ...state.config, calendar } })),
setTodoList: (todoList) =>
set((state) => ({ config: { ...state.config, todoList } })),
setPeople: (people) =>
set((state) => ({ config: { ...state.config, people } })),
setPackageSensor: (sensor) =>
set((state) => ({ config: { ...state.config, packageSensor: sensor } })),
setCameras: (cameras) =>
set((state) => ({ config: { ...state.config, cameras } })),
setPersonDetectionEntities: (entities) =>
set((state) => ({ config: { ...state.config, personDetectionEntities: entities } })),
setGo2rtcUrl: (url) =>
set((state) => ({ config: { ...state.config, go2rtcUrl: url } })),
setFrigateUrl: (url) =>
set((state) => ({ config: { ...state.config, frigateUrl: url } })),
setJellyfinUrl: (url) =>
set((state) => ({ config: { ...state.config, jellyfinUrl: url } })),
setJellyfinApiKey: (key) =>
set((state) => ({ config: { ...state.config, jellyfinApiKey: key } })),
setSetupCompleted: (completed) =>
set((state) => ({ config: { ...state.config, setupCompleted: completed } })),
updateConfig: (partial) =>
set((state) => ({ config: { ...state.config, ...partial } })),
resetConfig: () =>
set({ config: defaultConfig }),
}),
{
name: 'dashboard-settings',
version: CONFIG_VERSION,
migrate: (persistedState: unknown, _version: number) => {
const state = persistedState as { config: DashboardConfig } | undefined;
// If no persisted state, use defaults
if (!state?.config) {
return { config: { ...defaultConfig } };
}
// Merge: start with defaults, overlay user config, then force-update certain fields
const userConfig = state.config;
return {
config: {
...defaultConfig,
// Preserve user-configured entities if they exist
thermostats: userConfig.thermostats?.length > 0 ? userConfig.thermostats : defaultConfig.thermostats,
lights: userConfig.lights?.length > 0 ? userConfig.lights : defaultConfig.lights,
locks: userConfig.locks?.length > 0 ? userConfig.locks : defaultConfig.locks,
people: userConfig.people?.length > 0 ? userConfig.people : defaultConfig.people,
alarm: userConfig.alarm ?? defaultConfig.alarm,
calendar: userConfig.calendar ?? defaultConfig.calendar,
todoList: userConfig.todoList ?? defaultConfig.todoList,
packageSensor: userConfig.packageSensor ?? defaultConfig.packageSensor,
go2rtcUrl: userConfig.go2rtcUrl || defaultConfig.go2rtcUrl,
frigateUrl: userConfig.frigateUrl || defaultConfig.frigateUrl,
jellyfinUrl: userConfig.jellyfinUrl || defaultConfig.jellyfinUrl,
jellyfinApiKey: userConfig.jellyfinApiKey ?? defaultConfig.jellyfinApiKey,
// Always use latest camera defaults (user can't edit these in UI anyway)
cameras: defaultConfig.cameras,
personDetectionEntities: userConfig.personDetectionEntities?.length > 0
? userConfig.personDetectionEntities
: defaultConfig.personDetectionEntities,
setupCompleted: userConfig.setupCompleted ?? false,
configVersion: CONFIG_VERSION,
},
};
},
}
)
);
// Selector hooks
export const useConfig = () => useSettingsStore((state) => state.config);
export const useThermostats = () => useSettingsStore((state) => state.config.thermostats);
export const useLights = () => useSettingsStore((state) => state.config.lights);
export const useLocks = () => useSettingsStore((state) => state.config.locks);
export const useAlarmEntity = () => useSettingsStore((state) => state.config.alarm);
export const useCalendarEntity = () => useSettingsStore((state) => state.config.calendar);
export const useTodoEntity = () => useSettingsStore((state) => state.config.todoList);
export const usePeople = () => useSettingsStore((state) => state.config.people);
export const useCameras = () => useSettingsStore((state) => state.config.cameras);
// Helper to get lights grouped by room
export const useLightsByRoom = () => {
const lights = useLights();
return lights.reduce((acc, light) => {
if (!acc[light.room]) {
acc[light.room] = [];
}
acc[light.room].push(light);
return acc;
}, {} as Record<string, LightConfig[]>);
};