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:

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