- 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
260 lines
9.8 KiB
TypeScript
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[]>);
|
|
};
|