widgettdc-api / docs /SMART_HOME_WIDGET_ARCHITECTURE.md
Kraft102's picture
fix: sql.js Docker/Alpine compatibility layer for PatternMemory and FailureMemory
5a81b95
# 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<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:
```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 (
<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**
```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 && (
<SourceRecommendationPanel sources={recommendedSources} />
)}
```
- [ ] **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 (
<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