# Smart Home Widget Architecture - Auto-Discovery & Communication ## 🎯 CORE REQUIREMENTS Alle smart home widgets SKAL: 1. ✅ **Auto-discover datakilder** via SourceWidgetDiscovery 2. ✅ **Kommunikere med andre widgets** via WidgetCommunication 3. ✅ **Anbefale manglende kilder** til brugeren 4. ✅ **Subscribe til relevante data streams** via useLiveData --- ## 🏗️ WIDGET ARCHITECTURE ### Universal Widget Structure Alle widgets implementeres med samme base architecture: ```typescript import { useWidgetCommunication } from '@/contexts/WidgetContext'; import { useLiveData } from '@/hooks/useLiveData'; import { useSourceDiscovery } from '@/services/SourceWidgetDiscovery'; interface SmartHomeWidgetProps { widgetId: string; config?: Record; } export function SonosWidget({ widgetId, config }: SmartHomeWidgetProps) { // 1. AUTO-DISCOVERY: Connect to data sources automatically const { data: sonosData, connected, recommendedSources, connectionStatus } = useLiveData({ widgetId, widgetType: 'sonos', requiredSources: ['sonos-api', 'music-stream'], autoConnect: true, }); // 2. INTER-WIDGET COMMUNICATION: Send and receive events const { broadcastEvent, subscribeToEvent, getWidgetState } = useWidgetCommunication(widgetId); // 3. SOURCE RECOMMENDATIONS: Suggest missing sources const { discoverSources, canGenerateWidget } = useSourceDiscovery(); // Example: Broadcast when music starts playing const handlePlayMusic = async (track: Track) => { await playOnSonos(track); // Notify other widgets broadcastEvent({ type: 'music.started', source: widgetId, data: { track: track.name, artist: track.artist, room: getCurrentRoom(), } }); }; // Example: Listen for events from other widgets useEffect(() => { // If doorbell rings, pause music subscribeToEvent('doorbell.pressed', (event) => { pauseMusic(); setTimeout(() => resumeMusic(), 30000); // Resume after 30s }); // If TV turns on, lower music volume subscribeToEvent('tv.power.on', (event) => { setVolume(20); // Lower to background level }); }, []); return (
{/* Connection status indicator */} {/* Recommend missing sources */} {recommendedSources.length > 0 && ( enableSource(source)} /> )} {/* Widget content */} {connected && }
); } ``` --- ## 📡 DATA SOURCE AUTO-DISCOVERY ### How Widgets Find Their Data: ```typescript // Backend: Register all smart home sources // apps/backend/src/routes/acquisition.ts const SMART_HOME_SOURCES = [ { id: 'sonos-api', type: 'music', protocol: 'http', endpoint: process.env.SONOS_API_URL, capabilities: ['play', 'pause', 'volume', 'queue'], requiredBy: ['sonos', 'spotify', 'music-player'], }, { id: 'nest-camera-stream', type: 'camera', protocol: 'webrtc', endpoint: process.env.NEST_CAMERA_API, capabilities: ['live-stream', 'snapshot', 'motion-detect'], requiredBy: ['nest-camera', 'security-camera', 'doorbell'], }, { id: 'roomba-local', type: 'vacuum', protocol: 'rest', endpoint: 'http://roomba.local:8080', capabilities: ['start', 'stop', 'dock', 'status'], requiredBy: ['robot-vacuum'], }, // ... etc ]; // Auto-register on backend startup app.post('/api/acquisition/register-smart-home-sources', async (req, res) => { for (const source of SMART_HOME_SOURCES) { await sourceRegistry.registerSource(source); } res.json({ registered: SMART_HOME_SOURCES.length }); }); ``` ### Widget Auto-Connection Flow: ``` 1. Widget mounts → useLiveData({ widgetType: 'sonos' }) 2. Hook queries: GET /api/acquisition/sources?type=music 3. Backend returns matching sources: ['sonos-api', 'spotify-web-api'] 4. Widget auto-connects to available sources 5. If source missing → Recommend to user via SourceRecommendationPanel 6. User clicks "Enable" → POST /api/acquisition/enable-source 7. Widget reconnects automatically ``` --- ## 🔗 INTER-WIDGET COMMUNICATION ### Event Bus Architecture: ```typescript // Shared event types across all widgets export enum WidgetEventType { // Music events MUSIC_STARTED = 'music.started', MUSIC_PAUSED = 'music.paused', MUSIC_STOPPED = 'music.stopped', VOLUME_CHANGED = 'music.volume.changed', // TV/Media events TV_POWER_ON = 'tv.power.on', TV_POWER_OFF = 'tv.power.off', TV_INPUT_CHANGED = 'tv.input.changed', MEDIA_PLAYING = 'media.playing', // Security events DOORBELL_PRESSED = 'doorbell.pressed', MOTION_DETECTED = 'camera.motion', DOOR_UNLOCKED = 'lock.unlocked', ALARM_TRIGGERED = 'alarm.triggered', // Automation events VACUUM_STARTED = 'vacuum.started', VACUUM_FINISHED = 'vacuum.finished', MOWER_STARTED = 'mower.started', // Climate events TEMPERATURE_CHANGED = 'thermostat.temp.changed', HVAC_MODE_CHANGED = 'thermostat.mode.changed', // General PRESENCE_DETECTED = 'presence.detected', PRESENCE_LEFT = 'presence.left', } // Example: Sonos Widget listening to multiple events export function SonosWidget({ widgetId }: SmartHomeWidgetProps) { const { subscribeToEvent, broadcastEvent } = useWidgetCommunication(widgetId); useEffect(() => { // Pause music when doorbell rings const unsubDoorbell = subscribeToEvent( WidgetEventType.DOORBELL_PRESSED, (event) => { if (isPlaying()) { pauseMusic(); showNotification('Paused music - Doorbell'); } } ); // Lower volume when TV turns on const unsubTV = subscribeToEvent( WidgetEventType.TV_POWER_ON, (event) => { if (isPlaying() && getCurrentVolume() > 30) { setVolume(20); showNotification('Lowered volume - TV is on'); } } ); // Resume when vacuum finishes const unsubVacuum = subscribeToEvent( WidgetEventType.VACUUM_FINISHED, (event) => { if (wasPausedByAutomation) { resumeMusic(); } } ); return () => { unsubDoorbell(); unsubTV(); unsubVacuum(); }; }, []); } ``` --- ## 🤖 SMART AUTOMATION SCENARIOS ### Scenario 1: Movie Night Mode ```typescript // TV Widget broadcasts "movie mode starting" function TvWidget() { const startMovieMode = () => { broadcastEvent({ type: 'media.movie-mode.start', data: { movie: currentMovie } }); }; } // Sonos Widget lowers music function SonosWidget() { subscribeToEvent('media.movie-mode.start', () => { setVolume(5); }); } // Smart Lights Widget dims lights function LightsWidget() { subscribeToEvent('media.movie-mode.start', () => { setScene('movie'); setBrightness(20); }); } // Thermostat lowers temp slightly function ThermostatWidget() { subscribeToEvent('media.movie-mode.start', () => { setTemp(currentTemp - 1); }); } ``` ### Scenario 2: Vacuum Cleaning Coordination ```typescript // Robot Vacuum broadcasts start function VacuumWidget() { const startCleaning = () => { broadcastEvent({ type: 'vacuum.started', data: { room: 'living-room' } }); }; } // Sonos pauses music in that room function SonosWidget() { subscribeToEvent('vacuum.started', (event) => { if (event.data.room === currentRoom) { pauseMusic(); } }); } // Pet Feeder delays feeding function PetFeederWidget() { subscribeToEvent('vacuum.started', () => { delayNextFeeding(30); // Wait 30 min }); } ``` ### Scenario 3: Security Alert Chain ```typescript // Camera detects motion function CameraWidget() { onMotionDetected(() => { broadcastEvent({ type: 'camera.motion', data: { camera: 'front-door', snapshot: snapshotUrl, confidence: 0.95 } }); }); } // Smart Lock checks status function LockWidget() { subscribeToEvent('camera.motion', (event) => { if (!isLocked()) { showAlert('Motion detected - Door unlocked!'); } }); } // Lights turn on function LightsWidget() { subscribeToEvent('camera.motion', (event) => { if (isNightTime() && event.data.camera === 'front-door') { turnOnPorchLights(); } }); } // Sonos announces function SonosWidget() { subscribeToEvent('camera.motion', (event) => { if (event.data.confidence > 0.9) { announceOnSpeakers('Motion detected at front door'); } }); } ``` --- ## 📊 SOURCE RECOMMENDATION SYSTEM ### How Widgets Suggest Missing Sources: ```typescript // Backend endpoint to check widget requirements app.get('/api/widget-sources/:widgetType', async (req, res) => { const { widgetType } = req.params; const requirements = WIDGET_REQUIREMENTS[widgetType]; const availableSources = await sourceRegistry.getSources(); const missing = requirements.required.filter( req => !availableSources.find(s => s.id === req) ); const optional = requirements.optional.filter( opt => !availableSources.find(s => s.id === opt) ); res.json({ widgetType, available: availableSources, missing, optional, recommendations: missing.map(m => ({ sourceId: m, setupUrl: `/setup/${m}`, difficulty: 'easy', estimatedTime: '5 minutes' })) }); }); // Widget requirements mapping const WIDGET_REQUIREMENTS = { 'sonos': { required: ['sonos-api'], optional: ['spotify-web-api', 'apple-music-api'], }, 'nest-camera': { required: ['nest-camera-stream', 'google-sdm-api'], optional: ['object-detection-ml'], }, 'robot-vacuum': { required: ['roomba-local'], optional: ['floor-plan-image', 'home-assistant'], }, // ... }; ``` ### Frontend: Source Recommendation Panel ```typescript export function SourceRecommendationPanel({ sources, onEnable }) { return (
Missing Data Sources
{sources.map(source => (

{source.name}

{source.description}

))}
); } ``` --- ## 🔧 IMPLEMENTATION CHECKLIST ### For Each New Smart Home Widget: - [ ] **1. Define Widget Requirements** ```typescript const WIDGET_CONFIG = { type: 'sonos', requiredSources: ['sonos-api'], optionalSources: ['spotify-web-api'], requiredCapabilities: ['play', 'pause', 'volume'], }; ``` - [ ] **2. Implement useLiveData Hook** ```typescript const { data, connected, recommendedSources } = useLiveData({ widgetId, widgetType: 'sonos', autoConnect: true, }); ``` - [ ] **3. Setup Widget Communication** ```typescript const { broadcastEvent, subscribeToEvent } = useWidgetCommunication(widgetId); ``` - [ ] **4. Define Events Widget Broadcasts** ```typescript // On state change broadcastEvent({ type: 'music.started', data: {...} }); ``` - [ ] **5. Subscribe to Relevant Events** ```typescript subscribeToEvent('doorbell.pressed', handleDoorbell); subscribeToEvent('tv.power.on', handleTVOn); ``` - [ ] **6. Add Source Recommendations UI** ```typescript {recommendedSources.length > 0 && ( )} ``` - [ ] **7. Register Backend Source** ```typescript await sourceRegistry.registerSource({ id: 'sonos-api', type: 'music', endpoint: process.env.SONOS_API_URL, }); ``` - [ ] **8. Test Inter-Widget Communication** - Verify events broadcast correctly - Verify other widgets respond - Test multiple widgets simultaneously --- ## 🎯 EXAMPLE: Complete Sonos Widget ```typescript import React, { useEffect, useState } from 'react'; import { useLiveData } from '@/hooks/useLiveData'; import { useWidgetCommunication } from '@/contexts/WidgetContext'; import { SourceRecommendationPanel } from '@/components/SourceRecommendationPanel'; import { WidgetEventType } from '@/types/widget-events'; export function SonosWidget({ widgetId }: { widgetId: string }) { // 1. AUTO-CONNECT TO DATA SOURCES const { data: sonosData, connected, recommendedSources, connectionStatus, refetch, } = useLiveData({ widgetId, widgetType: 'sonos', requiredSources: ['sonos-api'], optionalSources: ['spotify-web-api'], autoConnect: true, pollInterval: 5000, }); // 2. WIDGET COMMUNICATION const { broadcastEvent, subscribeToEvent, getWidgetState } = useWidgetCommunication(widgetId); const [isPaused, setIsPaused] = useState(false); // 3. HANDLE EXTERNAL EVENTS useEffect(() => { // Pause on doorbell const unsubDoorbell = subscribeToEvent( WidgetEventType.DOORBELL_PRESSED, (event) => { if (sonosData?.isPlaying) { pauseMusic(); setTimeout(() => resumeMusic(), 30000); } } ); // Lower volume for TV const unsubTV = subscribeToEvent( WidgetEventType.TV_POWER_ON, (event) => { if (sonosData?.volume > 30) { setVolume(20); } } ); // Resume after vacuum const unsubVacuum = subscribeToEvent( WidgetEventType.VACUUM_FINISHED, () => { if (isPaused) { resumeMusic(); } } ); return () => { unsubDoorbell(); unsubTV(); unsubVacuum(); }; }, [sonosData, isPaused]); // 4. BROADCAST OWN EVENTS const handlePlay = async (track: Track) => { await playOnSonos(track); broadcastEvent({ type: WidgetEventType.MUSIC_STARTED, source: widgetId, data: { track: track.name, artist: track.artist, album: track.album, room: sonosData?.currentRoom, } }); }; const handleVolumeChange = async (newVolume: number) => { await setVolume(newVolume); broadcastEvent({ type: WidgetEventType.VOLUME_CHANGED, source: widgetId, data: { volume: newVolume, room: sonosData?.currentRoom, } }); }; return (
{/* Source Recommendations */} {recommendedSources.length > 0 && ( { await enableSource(source.id); refetch(); }} /> )} {/* Connection Status */} {/* Widget Content */} {connected && sonosData && (
handlePlay(sonosData.queue[0])} onPause={pauseMusic} />
)}
); } ``` --- ## 📚 COMPLETE WIDGET LIST WITH COMMUNICATION | Widget | Broadcasts | Listens To | |--------|-----------|------------| | **Sonos** | `music.started`, `music.paused`, `volume.changed` | `doorbell.pressed`, `tv.power.on`, `vacuum.started` | | **Nest Camera** | `motion.detected`, `person.detected`, `doorbell.pressed` | `alarm.triggered`, `presence.left` | | **Robot Vacuum** | `vacuum.started`, `vacuum.finished`, `vacuum.error` | `presence.detected`, `music.started` | | **Smart TV** | `tv.power.on`, `tv.power.off`, `media.playing` | `doorbell.pressed`, `alarm.triggered` | | **Thermostat** | `temp.changed`, `mode.changed` | `presence.detected`, `presence.left`, `media.movie-mode.start` | | **Smart Lock** | `lock.unlocked`, `lock.locked` | `motion.detected`, `presence.detected` | | **Lawn Mower** | `mower.started`, `mower.finished` | `weather.rain`, `presence.detected` | | **Lights** | `lights.on`, `lights.off`, `scene.changed` | `motion.detected`, `tv.power.on`, `presence.left` | --- **Architecture:** Auto-Discovery + Inter-Widget Communication **Backend:** Source Registry + Event Bus **Frontend:** useLiveData + useWidgetCommunication **Date:** 2025-12-10