diff --git a/src/App.tsx b/src/App.tsx index 44e9070..301a080 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,7 @@ import { LocksOverlay } from '@/components/locks'; import { ControlsOverlay } from '@/components/controls'; import { CalendarWidget } from '@/components/calendar'; import { TodoWidget } from '@/components/todo'; +import { ChoreChart } from '@/components/chores'; import { SettingsPanel, ConnectionModal } from '@/components/settings'; import { CameraOverlay } from '@/components/cameras'; import { CameraFeed } from '@/components/cameras/CameraFeed'; @@ -99,11 +100,16 @@ function DashboardContent() { )} - {config.todoList && ( -
- +
+ {config.todoList && ( +
+ +
+ )} +
+
- )} +
); } diff --git a/src/components/chores/ChoreChart.tsx b/src/components/chores/ChoreChart.tsx new file mode 100644 index 0000000..b6e100b --- /dev/null +++ b/src/components/chores/ChoreChart.tsx @@ -0,0 +1,168 @@ +import { useEffect, useState, useCallback } from 'react'; +import { useHAStore } from '@/stores/haStore'; +import { haConnection } from '@/services/homeAssistant/connection'; + +// Person color mapping (matches calendar eventColors) +const PEOPLE: Record = { + becca: { bg: '#14b8a6', text: '#ffffff' }, + chris: { bg: '#22c55e', text: '#ffffff' }, + arabella: { bg: '#ec4899', text: '#ffffff' }, +}; + +const FALLBACK_COLOR = { bg: '#94a3b8', text: '#ffffff' }; + +interface ChoreItem { + uid: string; + summary: string; + status: string; + person: string | null; + task: string; + color: { bg: string; text: string }; +} + +function parseChore(item: { uid: string; summary: string; status: string }): ChoreItem { + // "Becca: Do dishes" → person=Becca, task=Do dishes + // "Take out trash" → person=null, task=Take out trash + const colonIdx = item.summary.indexOf(':'); + let person: string | null = null; + let task = item.summary; + let color = FALLBACK_COLOR; + + if (colonIdx > 0 && colonIdx < 20) { + const prefix = item.summary.slice(0, colonIdx).trim().toLowerCase(); + if (PEOPLE[prefix]) { + person = item.summary.slice(0, colonIdx).trim(); + task = item.summary.slice(colonIdx + 1).trim(); + color = PEOPLE[prefix]; + } + } + + // Also check if any person name appears anywhere in summary + if (!person) { + for (const [name, c] of Object.entries(PEOPLE)) { + if (item.summary.toLowerCase().includes(name)) { + person = name.charAt(0).toUpperCase() + name.slice(1); + color = c; + break; + } + } + } + + return { uid: item.uid, summary: item.summary, status: item.status, person, task, color }; +} + +const CHORE_ENTITY = 'todo.chores'; + +export function ChoreChart() { + const isConnected = useHAStore((s) => s.connectionState === 'connected'); + const [chores, setChores] = useState([]); + + const fetchChores = useCallback(async () => { + const conn = haConnection.getConnection(); + if (!conn) return; + try { + const result = await conn.sendMessagePromise({ + type: 'call_service', + domain: 'todo', + service: 'get_items', + target: { entity_id: CHORE_ENTITY }, + return_response: true, + }) as { response?: Record }> }; + const items = result?.response?.[CHORE_ENTITY]?.items ?? []; + setChores(items.map(parseChore)); + } catch { + // Entity may not exist yet + } + }, []); + + useEffect(() => { + if (!isConnected) return; + fetchChores(); + const id = setInterval(fetchChores, 30000); + return () => clearInterval(id); + }, [isConnected, fetchChores]); + + const toggleChore = useCallback(async (uid: string, currentStatus: string) => { + const conn = haConnection.getConnection(); + if (!conn) return; + const newStatus = currentStatus === 'completed' ? 'needs_action' : 'completed'; + try { + await conn.sendMessagePromise({ + type: 'call_service', + domain: 'todo', + service: 'update_item', + target: { entity_id: CHORE_ENTITY }, + service_data: { item: uid, status: newStatus }, + }); + fetchChores(); + } catch { + // ignore + } + }, [fetchChores]); + + if (!chores.length) { + return ( +
+
+ + + + Chores +
+
+

+ Add a todo.chores list in HA +

+
+
+ ); + } + + return ( +
+
+ + + + Chores +
+
+ {chores.map((chore) => ( + + ))} +
+
+ ); +} diff --git a/src/components/chores/index.ts b/src/components/chores/index.ts new file mode 100644 index 0000000..e1d4656 --- /dev/null +++ b/src/components/chores/index.ts @@ -0,0 +1 @@ +export { ChoreChart } from './ChoreChart'; diff --git a/src/components/controls/ControlsOverlay.tsx b/src/components/controls/ControlsOverlay.tsx index 9d47222..f7afdac 100644 --- a/src/components/controls/ControlsOverlay.tsx +++ b/src/components/controls/ControlsOverlay.tsx @@ -26,27 +26,13 @@ export function ControlsOverlay() {
-
- {config.alarm && ( -
- -
- )} +
+ {config.alarm && } {config.thermostats.map((thermostat) => ( -
- -
+ ))} - {config.lights.length > 0 && ( -
- -
- )} - {config.locks.length > 0 && ( -
- -
- )} + {config.lights.length > 0 && } + {config.locks.length > 0 && }
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index b384a03..0765a4d 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,4 +1,4 @@ -import { useConnectionState, useEntityState, useEntityAttribute } from '@/stores/haStore'; +import { useConnectionState, useEntityState, useEntityAttribute, useHAStore } from '@/stores/haStore'; import { useUIStore } from '@/stores/uiStore'; import { useSettingsStore } from '@/stores/settingsStore'; import { env } from '@/config/environment'; @@ -80,6 +80,42 @@ function PackageStatus() { ); } +const WEATHER_ICONS: Record = { + 'clear-night': '🌙', + 'cloudy': '☁️', + 'exceptional': '⚠️', + 'fog': '🌫️', + 'hail': '🌨️', + 'lightning': '⚡', + 'lightning-rainy': '⛈️', + 'partlycloudy': '⛅', + 'pouring': '🌧️', + 'rainy': '🌧️', + 'snowy': '❄️', + 'snowy-rainy': '🌨️', + 'sunny': '☀️', + 'windy': '💨', + 'windy-variant': '💨', +}; + +function WeatherBadge() { + const weatherEntity = 'weather.forecast_home_2'; + const condition = useEntityState(weatherEntity) ?? ''; + const entities = useHAStore((s) => s.entities); + const attrs = entities[weatherEntity]?.attributes as Record | undefined; + const temp = attrs?.temperature as number | undefined; + + if (!temp) return null; + + const icon = WEATHER_ICONS[condition] || '🌡️'; + return ( +
+ {icon} + {Math.round(temp)}° +
+ ); +} + export function Header() { const connectionState = useConnectionState(); const cameraOverlayOpen = useUIStore((state) => state.cameraOverlayOpen); @@ -239,8 +275,9 @@ export function Header() { - {/* Right - Connection Status */} -
+ {/* Right - Weather + Connection */} +
+
{getConnectionText()}