- Add 100 blog posts covering AI, development, and tech topics - Add .env.example for environment configuration - Add accessibility and lighthouse audit scripts - Remove obsolete SEO reports and temporary files - Remove dev-dist build artifacts and backup files - Remove unused portrait images (moved/consolidated elsewhere) - Update contact form and component improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
810 lines
24 KiB
Markdown
810 lines
24 KiB
Markdown
# 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
- [React Documentation](https://react.dev/)
|
|
- [shadcn/ui Components](https://ui.shadcn.com/)
|
|
- [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
|
|
- [Home Assistant Dashboard](https://www.home-assistant.io/dashboards/)
|