Initial commit: Electron + React touchscreen kiosk dashboard for Home Assistant
This commit is contained in:
259
src/stores/settingsStore.ts
Normal file
259
src/stores/settingsStore.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
// Increment this when default cameras/config changes to force refresh
|
||||
const CONFIG_VERSION = 8;
|
||||
|
||||
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[]>);
|
||||
};
|
||||
Reference in New Issue
Block a user