Menu
Nazad na Blog
1 min read
Automatisierung

Smart Home Dashboard Development

Smart Home Dashboard mit React und WebSocket. Real-time Updates, Device Control und responsive Design für die Heimautomatisierung.

Smart Home DashboardReactWebSocketHome AutomationReal-time UIDevice Control
Smart Home Dashboard Development

Smart Home Dashboard Development

Meta-Description: Smart Home Dashboard mit React und WebSocket. Real-time Updates, Device Control und responsive Design für die Heimautomatisierung.

Keywords: Smart Home Dashboard, React, WebSocket, Home Automation, Real-time UI, Device Control, IoT Dashboard


Einführung

Ein Smart Home Dashboard ist die zentrale Steuerung für alle vernetzten Geräte. Mit React, WebSocket und modernem UI-Design bauen wir ein responsives Dashboard für Echtzeit-Monitoring und Gerätesteuerung.


Dashboard Architecture

┌─────────────────────────────────────────────────────────────┐
│              SMART HOME DASHBOARD ARCHITECTURE               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Frontend:                                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                 React + Next.js                      │   │
│  │  ┌───────────┐  ┌───────────┐  ┌───────────┐       │   │
│  │  │  Device   │  │   Room    │  │  Scenes   │       │   │
│  │  │  Cards    │  │   View    │  │  Manager  │       │   │
│  │  └───────────┘  └───────────┘  └───────────┘       │   │
│  │  ┌───────────┐  ┌───────────┐  ┌───────────┐       │   │
│  │  │  Energy   │  │  Climate  │  │  Security │       │   │
│  │  │  Monitor  │  │  Control  │  │  Panel    │       │   │
│  │  └───────────┘  └───────────┘  └───────────┘       │   │
│  └─────────────────────────────────────────────────────┘   │
│                           │                                 │
│                    WebSocket / REST                         │
│                           │                                 │
│  Backend:                 ▼                                 │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              API Gateway / BFF                       │   │
│  └─────────────────────────────────────────────────────┘   │
│                           │                                 │
│  ┌────────────┬───────────┴───────────┬────────────┐      │
│  │    MQTT    │     Home Assistant    │   Database │      │
│  │   Broker   │         API           │  (States)  │      │
│  └────────────┴───────────────────────┴────────────┘      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

React Dashboard Components

// components/dashboard/DeviceCard.tsx
'use client';

import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Switch } from '@/components/ui/switch';
import { Slider } from '@/components/ui/slider';
import { cn } from '@/lib/utils';

interface DeviceCardProps {
  device: {
    id: string;
    name: string;
    type: 'light' | 'switch' | 'sensor' | 'thermostat';
    state: 'on' | 'off';
    brightness?: number;
    temperature?: number;
    humidity?: number;
    setpoint?: number;
  };
  onStateChange: (id: string, state: Partial<typeof device>) => void;
}

export function DeviceCard({ device, onStateChange }: DeviceCardProps) {
  const [isLoading, setIsLoading] = useState(false);

  const handleToggle = async (checked: boolean) => {
    setIsLoading(true);
    await onStateChange(device.id, { state: checked ? 'on' : 'off' });
    setIsLoading(false);
  };

  const handleBrightness = async (value: number[]) => {
    await onStateChange(device.id, { brightness: value[0] });
  };

  const handleSetpoint = async (value: number[]) => {
    await onStateChange(device.id, { setpoint: value[0] });
  };

  return (
    <Card className={cn(
      'transition-all duration-200',
      device.state === 'on' && 'ring-2 ring-primary'
    )}>
      <CardHeader className="flex flex-row items-center justify-between pb-2">
        <CardTitle className="text-sm font-medium">{device.name}</CardTitle>
        {(device.type === 'light' || device.type === 'switch') && (
          <Switch
            checked={device.state === 'on'}
            onCheckedChange={handleToggle}
            disabled={isLoading}
          />
        )}
      </CardHeader>

      <CardContent>
        {/* Light with Brightness */}
        {device.type === 'light' && device.brightness !== undefined && (
          <div className="space-y-2">
            <div className="flex justify-between text-sm">
              <span>Brightness</span>
              <span>{device.brightness}%</span>
            </div>
            <Slider
              value={[device.brightness]}
              onValueChange={handleBrightness}
              max={100}
              step={1}
              disabled={device.state === 'off'}
            />
          </div>
        )}

        {/* Sensor Readings */}
        {device.type === 'sensor' && (
          <div className="grid grid-cols-2 gap-4">
            {device.temperature !== undefined && (
              <div className="text-center">
                <div className="text-2xl font-bold">{device.temperature}°C</div>
                <div className="text-xs text-muted-foreground">Temperature</div>
              </div>
            )}
            {device.humidity !== undefined && (
              <div className="text-center">
                <div className="text-2xl font-bold">{device.humidity}%</div>
                <div className="text-xs text-muted-foreground">Humidity</div>
              </div>
            )}
          </div>
        )}

        {/* Thermostat */}
        {device.type === 'thermostat' && (
          <div className="space-y-4">
            <div className="text-center">
              <div className="text-3xl font-bold">{device.temperature}°C</div>
              <div className="text-xs text-muted-foreground">Current</div>
            </div>
            <div className="space-y-2">
              <div className="flex justify-between text-sm">
                <span>Setpoint</span>
                <span>{device.setpoint}°C</span>
              </div>
              <Slider
                value={[device.setpoint || 20]}
                onValueChange={handleSetpoint}
                min={16}
                max={28}
                step={0.5}
              />
            </div>
          </div>
        )}
      </CardContent>
    </Card>
  );
}

Room View

// components/dashboard/RoomView.tsx
'use client';

import { useState, useEffect } from 'react';
import { DeviceCard } from './DeviceCard';
import { useSmartHome } from '@/hooks/useSmartHome';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
  Home,
  Sofa,
  Bed,
  UtensilsCrossed,
  Bath,
  TreeDeciduous
} from 'lucide-react';

const roomIcons: Record<string, any> = {
  all: Home,
  living_room: Sofa,
  bedroom: Bed,
  kitchen: UtensilsCrossed,
  bathroom: Bath,
  outdoor: TreeDeciduous
};

export function RoomView() {
  const { devices, rooms, updateDevice, isConnected } = useSmartHome();
  const [activeRoom, setActiveRoom] = useState('all');

  const filteredDevices = activeRoom === 'all'
    ? devices
    : devices.filter(d => d.room === activeRoom);

  return (
    <div className="space-y-6">
      {/* Connection Status */}
      <div className="flex items-center gap-2">
        <div className={`w-2 h-2 rounded-full ${
          isConnected ? 'bg-green-500' : 'bg-red-500'
        }`} />
        <span className="text-sm text-muted-foreground">
          {isConnected ? 'Connected' : 'Disconnected'}
        </span>
      </div>

      {/* Room Tabs */}
      <Tabs value={activeRoom} onValueChange={setActiveRoom}>
        <TabsList className="flex-wrap h-auto">
          <TabsTrigger value="all" className="gap-2">
            <Home className="w-4 h-4" />
            All
          </TabsTrigger>
          {rooms.map(room => {
            const Icon = roomIcons[room.id] || Home;
            return (
              <TabsTrigger key={room.id} value={room.id} className="gap-2">
                <Icon className="w-4 h-4" />
                {room.name}
              </TabsTrigger>
            );
          })}
        </TabsList>

        <TabsContent value={activeRoom} className="mt-6">
          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
            {filteredDevices.map(device => (
              <DeviceCard
                key={device.id}
                device={device}
                onStateChange={updateDevice}
              />
            ))}
          </div>

          {filteredDevices.length === 0 && (
            <div className="text-center py-12 text-muted-foreground">
              No devices in this room
            </div>
          )}
        </TabsContent>
      </Tabs>
    </div>
  );
}

WebSocket Hook

// hooks/useSmartHome.ts
'use client';

import { useState, useEffect, useCallback, useRef } from 'react';

interface Device {
  id: string;
  name: string;
  type: 'light' | 'switch' | 'sensor' | 'thermostat';
  room: string;
  state: 'on' | 'off';
  brightness?: number;
  temperature?: number;
  humidity?: number;
  setpoint?: number;
  lastUpdated: string;
}

interface Room {
  id: string;
  name: string;
}

interface SmartHomeState {
  devices: Device[];
  rooms: Room[];
  isConnected: boolean;
}

export function useSmartHome() {
  const [state, setState] = useState<SmartHomeState>({
    devices: [],
    rooms: [],
    isConnected: false
  });

  const wsRef = useRef<WebSocket | null>(null);
  const reconnectTimeoutRef = useRef<NodeJS.Timeout>();

  const connect = useCallback(() => {
    const wsUrl = process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:3001';
    const ws = new WebSocket(wsUrl);

    ws.onopen = () => {
      console.log('WebSocket connected');
      setState(prev => ({ ...prev, isConnected: true }));

      // Request initial state
      ws.send(JSON.stringify({ type: 'get_state' }));
    };

    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);

      switch (message.type) {
        case 'initial_state':
          setState(prev => ({
            ...prev,
            devices: message.devices,
            rooms: message.rooms
          }));
          break;

        case 'device_update':
          setState(prev => ({
            ...prev,
            devices: prev.devices.map(d =>
              d.id === message.device.id
                ? { ...d, ...message.device, lastUpdated: new Date().toISOString() }
                : d
            )
          }));
          break;

        case 'device_added':
          setState(prev => ({
            ...prev,
            devices: [...prev.devices, message.device]
          }));
          break;

        case 'device_removed':
          setState(prev => ({
            ...prev,
            devices: prev.devices.filter(d => d.id !== message.deviceId)
          }));
          break;
      }
    };

    ws.onclose = () => {
      console.log('WebSocket disconnected');
      setState(prev => ({ ...prev, isConnected: false }));

      // Reconnect after 5 seconds
      reconnectTimeoutRef.current = setTimeout(connect, 5000);
    };

    ws.onerror = (error) => {
      console.error('WebSocket error:', error);
    };

    wsRef.current = ws;
  }, []);

  useEffect(() => {
    connect();

    return () => {
      if (reconnectTimeoutRef.current) {
        clearTimeout(reconnectTimeoutRef.current);
      }
      wsRef.current?.close();
    };
  }, [connect]);

  const updateDevice = useCallback(async (
    deviceId: string,
    updates: Partial<Device>
  ) => {
    // Optimistic Update
    setState(prev => ({
      ...prev,
      devices: prev.devices.map(d =>
        d.id === deviceId ? { ...d, ...updates } : d
      )
    }));

    // Send to server
    wsRef.current?.send(JSON.stringify({
      type: 'update_device',
      deviceId,
      updates
    }));
  }, []);

  const executeScene = useCallback((sceneId: string) => {
    wsRef.current?.send(JSON.stringify({
      type: 'execute_scene',
      sceneId
    }));
  }, []);

  return {
    ...state,
    updateDevice,
    executeScene
  };
}

Scene Manager

// components/dashboard/SceneManager.tsx
'use client';

import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
  Sun,
  Moon,
  Film,
  Coffee,
  PartyPopper,
  Power
} from 'lucide-react';

interface Scene {
  id: string;
  name: string;
  icon: string;
  description: string;
}

const defaultScenes: Scene[] = [
  { id: 'morning', name: 'Good Morning', icon: 'sun', description: 'Wake up routine' },
  { id: 'movie', name: 'Movie Time', icon: 'film', description: 'Dim lights for movies' },
  { id: 'goodnight', name: 'Good Night', icon: 'moon', description: 'All lights off' },
  { id: 'party', name: 'Party Mode', icon: 'party', description: 'Colorful lights' },
  { id: 'away', name: 'Away', icon: 'power', description: 'Energy saving mode' },
  { id: 'coffee', name: 'Coffee Break', icon: 'coffee', description: 'Cozy atmosphere' }
];

const iconMap: Record<string, any> = {
  sun: Sun,
  moon: Moon,
  film: Film,
  coffee: Coffee,
  party: PartyPopper,
  power: Power
};

interface SceneManagerProps {
  onExecute: (sceneId: string) => void;
}

export function SceneManager({ onExecute }: SceneManagerProps) {
  const [activeScene, setActiveScene] = useState<string | null>(null);
  const [isExecuting, setIsExecuting] = useState(false);

  const handleExecute = async (sceneId: string) => {
    setIsExecuting(true);
    setActiveScene(sceneId);

    await onExecute(sceneId);

    setTimeout(() => {
      setIsExecuting(false);
    }, 1000);
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle>Scenes</CardTitle>
      </CardHeader>
      <CardContent>
        <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
          {defaultScenes.map(scene => {
            const Icon = iconMap[scene.icon] || Sun;
            const isActive = activeScene === scene.id;

            return (
              <Button
                key={scene.id}
                variant={isActive ? 'default' : 'outline'}
                className="h-auto py-4 flex flex-col gap-2"
                onClick={() => handleExecute(scene.id)}
                disabled={isExecuting}
              >
                <Icon className="w-6 h-6" />
                <span className="text-sm font-medium">{scene.name}</span>
              </Button>
            );
          })}
        </div>
      </CardContent>
    </Card>
  );
}

Climate Control

// components/dashboard/ClimateControl.tsx
'use client';

import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Thermometer, Droplets, Wind, Plus, Minus } from 'lucide-react';

interface ClimateControlProps {
  temperature: number;
  humidity: number;
  setpoint: number;
  mode: 'heat' | 'cool' | 'auto' | 'off';
  onSetpointChange: (value: number) => void;
  onModeChange: (mode: string) => void;
}

export function ClimateControl({
  temperature,
  humidity,
  setpoint,
  mode,
  onSetpointChange,
  onModeChange
}: ClimateControlProps) {
  const adjustSetpoint = (delta: number) => {
    const newValue = Math.max(16, Math.min(28, setpoint + delta));
    onSetpointChange(newValue);
  };

  return (
    <Card>
      <CardHeader>
        <CardTitle className="flex items-center gap-2">
          <Thermometer className="w-5 h-5" />
          Climate
        </CardTitle>
      </CardHeader>
      <CardContent className="space-y-6">
        {/* Current Values */}
        <div className="grid grid-cols-2 gap-4">
          <div className="text-center p-4 bg-muted rounded-lg">
            <Thermometer className="w-6 h-6 mx-auto mb-2 text-orange-500" />
            <div className="text-3xl font-bold">{temperature}°C</div>
            <div className="text-sm text-muted-foreground">Current</div>
          </div>
          <div className="text-center p-4 bg-muted rounded-lg">
            <Droplets className="w-6 h-6 mx-auto mb-2 text-blue-500" />
            <div className="text-3xl font-bold">{humidity}%</div>
            <div className="text-sm text-muted-foreground">Humidity</div>
          </div>
        </div>

        {/* Setpoint Control */}
        <div className="flex items-center justify-center gap-6">
          <Button
            variant="outline"
            size="icon"
            className="w-12 h-12 rounded-full"
            onClick={() => adjustSetpoint(-0.5)}
          >
            <Minus className="w-6 h-6" />
          </Button>

          <div className="text-center">
            <div className="text-5xl font-bold">{setpoint}°C</div>
            <div className="text-sm text-muted-foreground">Target</div>
          </div>

          <Button
            variant="outline"
            size="icon"
            className="w-12 h-12 rounded-full"
            onClick={() => adjustSetpoint(0.5)}
          >
            <Plus className="w-6 h-6" />
          </Button>
        </div>

        {/* Mode Selection */}
        <div className="flex gap-2">
          {['heat', 'cool', 'auto', 'off'].map(m => (
            <Button
              key={m}
              variant={mode === m ? 'default' : 'outline'}
              className="flex-1"
              onClick={() => onModeChange(m)}
            >
              {m.charAt(0).toUpperCase() + m.slice(1)}
            </Button>
          ))}
        </div>
      </CardContent>
    </Card>
  );
}

Energy Monitor

// components/dashboard/EnergyMonitor.tsx
'use client';

import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { Zap, TrendingDown, TrendingUp } from 'lucide-react';

interface EnergyData {
  currentPower: number;  // Watts
  todayUsage: number;    // kWh
  monthlyUsage: number;  // kWh
  monthlyBudget: number; // kWh
  trend: 'up' | 'down' | 'stable';
  trendPercent: number;
}

interface EnergyMonitorProps {
  data: EnergyData;
}

export function EnergyMonitor({ data }: EnergyMonitorProps) {
  const budgetUsage = (data.monthlyUsage / data.monthlyBudget) * 100;
  const TrendIcon = data.trend === 'up' ? TrendingUp : TrendingDown;

  return (
    <Card>
      <CardHeader>
        <CardTitle className="flex items-center gap-2">
          <Zap className="w-5 h-5 text-yellow-500" />
          Energy
        </CardTitle>
      </CardHeader>
      <CardContent className="space-y-6">
        {/* Current Power */}
        <div className="text-center">
          <div className="text-4xl font-bold">
            {data.currentPower.toLocaleString()} W
          </div>
          <div className="text-sm text-muted-foreground">Current Power</div>
        </div>

        {/* Usage Stats */}
        <div className="grid grid-cols-2 gap-4">
          <div className="text-center p-3 bg-muted rounded-lg">
            <div className="text-xl font-semibold">
              {data.todayUsage.toFixed(1)} kWh
            </div>
            <div className="text-xs text-muted-foreground">Today</div>
          </div>
          <div className="text-center p-3 bg-muted rounded-lg">
            <div className="text-xl font-semibold">
              {data.monthlyUsage.toFixed(0)} kWh
            </div>
            <div className="text-xs text-muted-foreground">This Month</div>
          </div>
        </div>

        {/* Budget Progress */}
        <div className="space-y-2">
          <div className="flex justify-between text-sm">
            <span>Monthly Budget</span>
            <span>{budgetUsage.toFixed(0)}%</span>
          </div>
          <Progress value={budgetUsage} className="h-2" />
          <div className="text-xs text-muted-foreground text-right">
            {data.monthlyUsage.toFixed(0)} / {data.monthlyBudget} kWh
          </div>
        </div>

        {/* Trend */}
        <div className="flex items-center justify-center gap-2">
          <TrendIcon className={`w-5 h-5 ${
            data.trend === 'up' ? 'text-red-500' : 'text-green-500'
          }`} />
          <span className={`font-medium ${
            data.trend === 'up' ? 'text-red-500' : 'text-green-500'
          }`}>
            {data.trend === 'up' ? '+' : '-'}{data.trendPercent}%
          </span>
          <span className="text-sm text-muted-foreground">vs last week</span>
        </div>
      </CardContent>
    </Card>
  );
}

Main Dashboard Layout

// app/dashboard/page.tsx
'use client';

import { RoomView } from '@/components/dashboard/RoomView';
import { SceneManager } from '@/components/dashboard/SceneManager';
import { ClimateControl } from '@/components/dashboard/ClimateControl';
import { EnergyMonitor } from '@/components/dashboard/EnergyMonitor';
import { useSmartHome } from '@/hooks/useSmartHome';

export default function DashboardPage() {
  const { devices, executeScene, updateDevice } = useSmartHome();

  // Aggregate climate data
  const climateDevice = devices.find(d => d.type === 'thermostat');

  // Mock energy data (in production: from API)
  const energyData = {
    currentPower: 1234,
    todayUsage: 12.5,
    monthlyUsage: 245,
    monthlyBudget: 300,
    trend: 'down' as const,
    trendPercent: 8
  };

  return (
    <div className="min-h-screen bg-background p-6">
      <header className="mb-8">
        <h1 className="text-3xl font-bold">Smart Home</h1>
        <p className="text-muted-foreground">Welcome back!</p>
      </header>

      <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
        {/* Main Area - Devices */}
        <div className="lg:col-span-2 space-y-6">
          <RoomView />
        </div>

        {/* Sidebar */}
        <div className="space-y-6">
          <SceneManager onExecute={executeScene} />

          {climateDevice && (
            <ClimateControl
              temperature={climateDevice.temperature || 21}
              humidity={50}
              setpoint={climateDevice.setpoint || 21}
              mode="auto"
              onSetpointChange={(value) =>
                updateDevice(climateDevice.id, { setpoint: value })
              }
              onModeChange={(mode) =>
                console.log('Mode:', mode)
              }
            />
          )}

          <EnergyMonitor data={energyData} />
        </div>
      </div>
    </div>
  );
}

Fazit

Smart Home Dashboard mit React bietet:

  1. Real-time Updates: WebSocket für Live-Daten
  2. Intuitive UI: Room-basierte Navigation
  3. Responsive Design: Desktop & Mobile
  4. Szenen & Automation: One-Click Control

Das perfekte Interface für Smart Home Control.


Bildprompts

  1. "Smart home dashboard on tablet, dark mode with glowing device cards"
  2. "Room view with connected devices, modern UI design"
  3. "Energy monitoring graph with real-time power consumption"

Quellen