Spaces:
Paused
Paused
Smart Home Widget Architecture - Auto-Discovery & Communication
π― CORE REQUIREMENTS
Alle smart home widgets SKAL:
- β Auto-discover datakilder via SourceWidgetDiscovery
- β Kommunikere med andre widgets via WidgetCommunication
- β Anbefale manglende kilder til brugeren
- β Subscribe til relevante data streams via useLiveData
ποΈ WIDGET ARCHITECTURE
Universal Widget Structure
Alle widgets implementeres med samme base architecture:
import { useWidgetCommunication } from '@/contexts/WidgetContext';
import { useLiveData } from '@/hooks/useLiveData';
import { useSourceDiscovery } from '@/services/SourceWidgetDiscovery';
interface SmartHomeWidgetProps {
widgetId: string;
config?: Record<string, any>;
}
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 (
<div className="sonos-widget">
{/* Connection status indicator */}
<ConnectionIndicator status={connectionStatus} />
{/* Recommend missing sources */}
{recommendedSources.length > 0 && (
<SourceRecommendationPanel
sources={recommendedSources}
onEnable={(source) => enableSource(source)}
/>
)}
{/* Widget content */}
{connected && <MusicPlayer data={sonosData} />}
</div>
);
}
π‘ DATA SOURCE AUTO-DISCOVERY
How Widgets Find Their Data:
// 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:
// 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
// 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
// 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
// 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:
// 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
export function SourceRecommendationPanel({ sources, onEnable }) {
return (
<div className="bg-orange-500/10 border border-orange-500/30 rounded p-3 mb-3">
<div className="flex items-center gap-2 mb-2">
<AlertTriangle className="w-4 h-4 text-orange-400" />
<span className="text-sm font-medium">Missing Data Sources</span>
</div>
{sources.map(source => (
<div key={source.id} className="flex items-center justify-between mb-2">
<div>
<p className="text-xs font-medium">{source.name}</p>
<p className="text-xs opacity-60">{source.description}</p>
</div>
<button
onClick={() => onEnable(source)}
className="px-3 py-1 bg-orange-500 text-white rounded text-xs"
>
Enable
</button>
</div>
))}
</div>
);
}
π§ IMPLEMENTATION CHECKLIST
For Each New Smart Home Widget:
1. Define Widget Requirements
const WIDGET_CONFIG = { type: 'sonos', requiredSources: ['sonos-api'], optionalSources: ['spotify-web-api'], requiredCapabilities: ['play', 'pause', 'volume'], };2. Implement useLiveData Hook
const { data, connected, recommendedSources } = useLiveData({ widgetId, widgetType: 'sonos', autoConnect: true, });3. Setup Widget Communication
const { broadcastEvent, subscribeToEvent } = useWidgetCommunication(widgetId);4. Define Events Widget Broadcasts
// On state change broadcastEvent({ type: 'music.started', data: {...} });5. Subscribe to Relevant Events
subscribeToEvent('doorbell.pressed', handleDoorbell); subscribeToEvent('tv.power.on', handleTVOn);6. Add Source Recommendations UI
{recommendedSources.length > 0 && ( <SourceRecommendationPanel sources={recommendedSources} /> )}7. Register Backend Source
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
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 (
<div className="sonos-widget">
{/* Source Recommendations */}
{recommendedSources.length > 0 && (
<SourceRecommendationPanel
sources={recommendedSources}
onEnable={async (source) => {
await enableSource(source.id);
refetch();
}}
/>
)}
{/* Connection Status */}
<ConnectionIndicator
status={connectionStatus}
sources={connected ? sonosData?.sources : []}
/>
{/* Widget Content */}
{connected && sonosData && (
<div className="music-player">
<NowPlaying track={sonosData.currentTrack} />
<VolumeControl
volume={sonosData.volume}
onChange={handleVolumeChange}
/>
<PlaybackControls
isPlaying={sonosData.isPlaying}
onPlay={() => handlePlay(sonosData.queue[0])}
onPause={pauseMusic}
/>
</div>
)}
</div>
);
}
π 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