HA controls + wider layout: toggle lights/switches, cover controls, more sensors
- Backend: call_ha_service() for controlling entities via HA REST API - Backend: POST /api/ha/control with JWT auth + cache invalidation - Backend: Parse switches, binary_sensors, humidity, climate entities - Frontend: HA card now xl:col-span-2 (double width) - Frontend: Interactive toggles for lights/switches, cover up/stop/down - Frontend: Temperature + humidity sensors, climate display, binary sensors - Frontend: Two-column internal layout (controls left, sensors right) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4e7c1909ee
commit
f5b7c53f18
6 changed files with 683 additions and 168 deletions
|
|
@ -66,13 +66,65 @@ export interface ServersResponse {
|
|||
servers: ServerStats[];
|
||||
}
|
||||
|
||||
export interface HALight {
|
||||
entity_id: string;
|
||||
name: string;
|
||||
state: string;
|
||||
brightness: number | null;
|
||||
color_mode?: string;
|
||||
}
|
||||
|
||||
export interface HASwitch {
|
||||
entity_id: string;
|
||||
name: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
export interface HACover {
|
||||
entity_id: string;
|
||||
name: string;
|
||||
state: string;
|
||||
current_position: number | null;
|
||||
device_class?: string;
|
||||
}
|
||||
|
||||
export interface HASensor {
|
||||
entity_id: string;
|
||||
name: string;
|
||||
state: number | string;
|
||||
unit: string;
|
||||
device_class?: string;
|
||||
}
|
||||
|
||||
export interface HABinarySensor {
|
||||
entity_id: string;
|
||||
name: string;
|
||||
state: string;
|
||||
device_class: string;
|
||||
}
|
||||
|
||||
export interface HAClimate {
|
||||
entity_id: string;
|
||||
name: string;
|
||||
state: string;
|
||||
current_temperature: number | null;
|
||||
target_temperature: number | null;
|
||||
hvac_modes: string[];
|
||||
humidity: number | null;
|
||||
}
|
||||
|
||||
export interface HAData {
|
||||
online: boolean;
|
||||
lights: { entity_id: string; name: string; state: string; brightness: number }[];
|
||||
covers: { entity_id: string; name: string; state: string; position: number }[];
|
||||
sensors: { entity_id: string; name: string; state: number; unit: string }[];
|
||||
lights: HALight[];
|
||||
switches: HASwitch[];
|
||||
covers: HACover[];
|
||||
sensors: HASensor[];
|
||||
binary_sensors: HABinarySensor[];
|
||||
climate: HAClimate[];
|
||||
lights_on: number;
|
||||
lights_total: number;
|
||||
switches_on: number;
|
||||
switches_total: number;
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -130,6 +182,22 @@ export const fetchNews = (limit = 20, offset = 0, category?: string) => {
|
|||
};
|
||||
export const fetchServers = () => fetchJSON<ServersResponse>("/servers");
|
||||
export const fetchHA = () => fetchJSON<HAData>("/ha");
|
||||
export const controlHA = async (
|
||||
entity_id: string,
|
||||
action: string,
|
||||
data?: Record<string, unknown>,
|
||||
): Promise<{ ok: boolean; error?: string }> => {
|
||||
const token = localStorage.getItem("token");
|
||||
const res = await fetch(`${API_BASE}/ha/control`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: JSON.stringify({ entity_id, action, data }),
|
||||
});
|
||||
return res.json();
|
||||
};
|
||||
export const fetchTasks = () => fetchJSON<TasksResponse>("/tasks");
|
||||
export const fetchMqtt = () => fetchJSON<MqttData>("/mqtt");
|
||||
export const fetchAll = () => fetchJSON<DashboardData>("/all");
|
||||
|
|
|
|||
|
|
@ -1,16 +1,62 @@
|
|||
import { Home, Lightbulb, ArrowUp, ArrowDown, Thermometer, WifiOff } from "lucide-react";
|
||||
import { useState, useCallback } from "react";
|
||||
import {
|
||||
Home,
|
||||
Lightbulb,
|
||||
Power,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
Square,
|
||||
Thermometer,
|
||||
Droplets,
|
||||
DoorOpen,
|
||||
DoorClosed,
|
||||
Eye,
|
||||
Flame,
|
||||
WifiOff,
|
||||
} from "lucide-react";
|
||||
import type { HAData } from "../api";
|
||||
import { controlHA } from "../api";
|
||||
|
||||
/* ── Types ───────────────────────────────────────────────── */
|
||||
|
||||
interface HomeAssistantProps {
|
||||
data: HAData;
|
||||
}
|
||||
|
||||
/* ── Main Component ──────────────────────────────────────── */
|
||||
|
||||
export default function HomeAssistant({ data }: HomeAssistantProps) {
|
||||
const [pending, setPending] = useState<Set<string>>(new Set());
|
||||
|
||||
const handleControl = useCallback(
|
||||
async (entity_id: string, action: string) => {
|
||||
setPending((p) => new Set(p).add(entity_id));
|
||||
try {
|
||||
const result = await controlHA(entity_id, action);
|
||||
if (!result.ok) {
|
||||
console.error(`[HA] Control failed: ${result.error}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[HA] Control error:", err);
|
||||
} finally {
|
||||
// Small delay so HA can process, then let parent refresh
|
||||
setTimeout(() => {
|
||||
setPending((p) => {
|
||||
const next = new Set(p);
|
||||
next.delete(entity_id);
|
||||
return next;
|
||||
});
|
||||
}, 600);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
if (data.error) {
|
||||
return (
|
||||
<div className="deck-card p-5 animate-fade-in">
|
||||
<div className="deck-card p-5 animate-fade-in h-full">
|
||||
<div className="flex items-center gap-3 text-cherry">
|
||||
<WifiOff className="w-5 h-5" />
|
||||
<div>
|
||||
|
|
@ -22,147 +68,342 @@ export default function HomeAssistant({ data }: HomeAssistantProps) {
|
|||
);
|
||||
}
|
||||
|
||||
// Split sensors by type
|
||||
const tempSensors = data.sensors.filter((s) => s.device_class === "temperature");
|
||||
const humiditySensors = data.sensors.filter((s) => s.device_class === "humidity");
|
||||
|
||||
return (
|
||||
<div className="deck-card p-5 animate-fade-in" data-accent="iris">
|
||||
<div className="deck-card p-5 animate-fade-in h-full" data-accent="iris">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<Home className="w-4 h-4 text-iris" />
|
||||
<h3 className="text-sm font-semibold text-base-900">Home Assistant</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`status-dot ${data.online ? "bg-mint" : "bg-cherry"}`} />
|
||||
<span className={`text-[10px] font-mono font-medium ${data.online ? "text-mint" : "text-cherry"}`}>
|
||||
{data.online ? "Online" : "Offline"}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="tag border-iris/30 text-iris bg-iris/5">
|
||||
{data.lights_on + data.switches_on} aktiv
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`status-dot ${data.online ? "bg-mint" : "bg-cherry"}`} />
|
||||
<span
|
||||
className={`text-[10px] font-mono font-medium ${data.online ? "text-mint" : "text-cherry"}`}
|
||||
>
|
||||
{data.online ? "Online" : "Offline"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
{/* Lights */}
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Lightbulb className="w-3 h-3 text-base-500" />
|
||||
<span className="data-label">Lichter</span>
|
||||
</div>
|
||||
<span className="tag border-iris/30 text-iris bg-iris/5">
|
||||
{data.lights_on}/{data.lights_total}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{data.lights.length > 0 ? (
|
||||
<div className="grid grid-cols-4 gap-px bg-base-300">
|
||||
{data.lights.map((light) => {
|
||||
const isOn = light.state === "on";
|
||||
return (
|
||||
<div
|
||||
key={light.entity_id}
|
||||
className={`flex flex-col items-center gap-1.5 px-2 py-3 transition-colors ${
|
||||
isOn ? "bg-gold/[0.08]" : "bg-base-50"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`w-2 h-2 transition-all ${
|
||||
isOn
|
||||
? "bg-gold shadow-[0_0_8px_#e8a44a80]"
|
||||
: "bg-base-400"
|
||||
}`}
|
||||
style={{ borderRadius: 0 }}
|
||||
/>
|
||||
<span className="text-[9px] text-center text-base-600 leading-tight truncate w-full font-mono">
|
||||
{light.name}
|
||||
</span>
|
||||
{isOn && light.brightness > 0 && (
|
||||
<span className="text-[9px] font-mono text-gold/70">
|
||||
{Math.round((light.brightness / 255) * 100)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-base-500">Keine Lichter konfiguriert</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Covers */}
|
||||
{data.covers.length > 0 && (
|
||||
{/* Two-column layout */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
{/* ── Left Column: Lights + Switches ────────────── */}
|
||||
<div className="space-y-5">
|
||||
{/* Lights */}
|
||||
<section>
|
||||
<div className="flex items-center gap-1.5 mb-3">
|
||||
<ArrowUp className="w-3 h-3 text-base-500" />
|
||||
<span className="data-label">Rollos</span>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Lightbulb className="w-3 h-3 text-base-500" />
|
||||
<span className="data-label">Lichter</span>
|
||||
</div>
|
||||
<span className="tag border-gold/30 text-gold bg-gold/5">
|
||||
{data.lights_on}/{data.lights_total}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-px">
|
||||
{data.covers.map((cover) => {
|
||||
const isOpen = cover.state === "open";
|
||||
const isClosed = cover.state === "closed";
|
||||
return (
|
||||
<div
|
||||
key={cover.entity_id}
|
||||
className="flex items-center justify-between px-3 py-2 bg-base-100 border-l-2 border-base-300 hover:border-iris transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{isOpen ? (
|
||||
<ArrowUp className="w-3 h-3 text-mint" />
|
||||
) : isClosed ? (
|
||||
<ArrowDown className="w-3 h-3 text-base-500" />
|
||||
) : (
|
||||
<ArrowDown className="w-3 h-3 text-gold" />
|
||||
)}
|
||||
<span className="text-xs text-base-700">{cover.name}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{cover.position > 0 && (
|
||||
<span className="text-[10px] font-mono text-base-500">
|
||||
{cover.position}%
|
||||
{data.lights.length > 0 ? (
|
||||
<div className="grid grid-cols-3 gap-px bg-base-300">
|
||||
{data.lights.map((light) => {
|
||||
const isOn = light.state === "on";
|
||||
const isPending = pending.has(light.entity_id);
|
||||
return (
|
||||
<button
|
||||
key={light.entity_id}
|
||||
onClick={() => handleControl(light.entity_id, "toggle")}
|
||||
disabled={isPending}
|
||||
className={`flex flex-col items-center gap-1.5 px-2 py-3 transition-all cursor-pointer
|
||||
${isOn ? "bg-gold/[0.08] hover:bg-gold/[0.15]" : "bg-base-50 hover:bg-base-100"}
|
||||
${isPending ? "opacity-50 animate-pulse" : ""}`}
|
||||
title={`${light.name} ${isOn ? "ausschalten" : "einschalten"}`}
|
||||
>
|
||||
<div
|
||||
className={`w-2.5 h-2.5 transition-all ${
|
||||
isOn
|
||||
? "bg-gold shadow-[0_0_10px_#e8a44a80]"
|
||||
: "bg-base-400"
|
||||
}`}
|
||||
/>
|
||||
<span className="text-[9px] text-center text-base-600 leading-tight truncate w-full font-mono">
|
||||
{light.name}
|
||||
</span>
|
||||
{isOn && light.brightness != null && light.brightness > 0 && (
|
||||
<span className="text-[9px] font-mono text-gold/70">
|
||||
{light.brightness}%
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-base-500 italic">Keine Lichter</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Switches */}
|
||||
{data.switches.length > 0 && (
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Power className="w-3 h-3 text-base-500" />
|
||||
<span className="data-label">Schalter</span>
|
||||
</div>
|
||||
<span className="tag border-mint/30 text-mint bg-mint/5">
|
||||
{data.switches_on}/{data.switches_total}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-px">
|
||||
{data.switches.map((sw) => {
|
||||
const isOn = sw.state === "on";
|
||||
const isPending = pending.has(sw.entity_id);
|
||||
return (
|
||||
<button
|
||||
key={sw.entity_id}
|
||||
onClick={() => handleControl(sw.entity_id, "toggle")}
|
||||
disabled={isPending}
|
||||
className={`flex items-center justify-between w-full px-3 py-2 transition-all cursor-pointer
|
||||
border-l-2 ${isOn ? "border-mint bg-mint/[0.05] hover:bg-mint/[0.10]" : "border-base-300 bg-base-100 hover:bg-base-200"}
|
||||
${isPending ? "opacity-50 animate-pulse" : ""}`}
|
||||
title={`${sw.name} ${isOn ? "ausschalten" : "einschalten"}`}
|
||||
>
|
||||
<span className="text-xs text-base-700 truncate mr-2">{sw.name}</span>
|
||||
<span
|
||||
className={`text-[10px] font-mono font-medium ${
|
||||
isOpen ? "text-mint" : isClosed ? "text-base-500" : "text-gold"
|
||||
className={`text-[10px] font-mono font-medium flex-shrink-0 ${
|
||||
isOn ? "text-mint" : "text-base-500"
|
||||
}`}
|
||||
>
|
||||
{isOpen ? "Offen" : isClosed ? "Zu" : cover.state}
|
||||
{isOn ? "AN" : "AUS"}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Right Column: Sensors + Covers + Binary + Climate ── */}
|
||||
<div className="space-y-5">
|
||||
{/* Temperature Sensors */}
|
||||
{tempSensors.length > 0 && (
|
||||
<section>
|
||||
<div className="flex items-center gap-1.5 mb-3">
|
||||
<Thermometer className="w-3 h-3 text-base-500" />
|
||||
<span className="data-label">Temperaturen</span>
|
||||
</div>
|
||||
<div className="space-y-px">
|
||||
{tempSensors.map((sensor) => (
|
||||
<div
|
||||
key={sensor.entity_id}
|
||||
className="flex items-center justify-between px-3 py-2 bg-base-100 border-l-2 border-base-300 hover:border-iris transition-colors"
|
||||
>
|
||||
<span className="text-xs text-base-700 truncate mr-2">{sensor.name}</span>
|
||||
<span className="text-sm data-value text-base-900 flex-shrink-0">
|
||||
{typeof sensor.state === "number" ? sensor.state.toFixed(1) : sensor.state}
|
||||
<span className="text-[10px] text-base-500 ml-0.5">{sensor.unit}</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Humidity Sensors */}
|
||||
{humiditySensors.length > 0 && (
|
||||
<section>
|
||||
<div className="flex items-center gap-1.5 mb-3">
|
||||
<Droplets className="w-3 h-3 text-base-500" />
|
||||
<span className="data-label">Luftfeuchte</span>
|
||||
</div>
|
||||
<div className="space-y-px">
|
||||
{humiditySensors.map((sensor) => (
|
||||
<div
|
||||
key={sensor.entity_id}
|
||||
className="flex items-center justify-between px-3 py-2 bg-base-100 border-l-2 border-base-300 hover:border-azure transition-colors"
|
||||
>
|
||||
<span className="text-xs text-base-700 truncate mr-2">{sensor.name}</span>
|
||||
<span className="text-sm data-value text-base-900 flex-shrink-0">
|
||||
{typeof sensor.state === "number" ? sensor.state.toFixed(1) : sensor.state}
|
||||
<span className="text-[10px] text-base-500 ml-0.5">{sensor.unit}</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Climate */}
|
||||
{data.climate.length > 0 && (
|
||||
<section>
|
||||
<div className="flex items-center gap-1.5 mb-3">
|
||||
<Flame className="w-3 h-3 text-base-500" />
|
||||
<span className="data-label">Klima</span>
|
||||
</div>
|
||||
<div className="space-y-px">
|
||||
{data.climate.map((cl) => (
|
||||
<div
|
||||
key={cl.entity_id}
|
||||
className="px-3 py-2 bg-base-100 border-l-2 border-cherry/40"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-base-700">{cl.name}</span>
|
||||
<span
|
||||
className={`text-[10px] font-mono font-medium ${
|
||||
cl.state === "heat"
|
||||
? "text-cherry"
|
||||
: cl.state === "cool"
|
||||
? "text-azure"
|
||||
: "text-base-500"
|
||||
}`}
|
||||
>
|
||||
{cl.state === "heat" ? "Heizen" : cl.state === "cool" ? "Kühlen" : cl.state === "off" ? "Aus" : cl.state}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-1">
|
||||
{cl.current_temperature != null && (
|
||||
<span className="text-sm data-value text-base-900">
|
||||
{cl.current_temperature.toFixed(1)}
|
||||
<span className="text-[10px] text-base-500 ml-0.5">°C</span>
|
||||
</span>
|
||||
)}
|
||||
{cl.target_temperature != null && (
|
||||
<span className="text-[10px] font-mono text-base-500">
|
||||
Ziel: {cl.target_temperature.toFixed(1)}°C
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Temperature Sensors */}
|
||||
{data.sensors.length > 0 && (
|
||||
<section>
|
||||
<div className="flex items-center gap-1.5 mb-3">
|
||||
<Thermometer className="w-3 h-3 text-base-500" />
|
||||
<span className="data-label">Temperaturen</span>
|
||||
</div>
|
||||
{/* Covers */}
|
||||
{data.covers.length > 0 && (
|
||||
<section>
|
||||
<div className="flex items-center gap-1.5 mb-3">
|
||||
<ArrowUp className="w-3 h-3 text-base-500" />
|
||||
<span className="data-label">Rollos</span>
|
||||
</div>
|
||||
<div className="space-y-px">
|
||||
{data.covers.map((cover) => {
|
||||
const isOpen = cover.state === "open";
|
||||
const isClosed = cover.state === "closed";
|
||||
const isPending = pending.has(cover.entity_id);
|
||||
return (
|
||||
<div
|
||||
key={cover.entity_id}
|
||||
className={`flex items-center justify-between px-3 py-2 bg-base-100 border-l-2 transition-colors
|
||||
${isOpen ? "border-mint" : isClosed ? "border-base-300" : "border-gold"}
|
||||
${isPending ? "opacity-50 animate-pulse" : ""}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{isOpen ? (
|
||||
<ArrowUp className="w-3 h-3 text-mint flex-shrink-0" />
|
||||
) : isClosed ? (
|
||||
<ArrowDown className="w-3 h-3 text-base-500 flex-shrink-0" />
|
||||
) : (
|
||||
<ArrowDown className="w-3 h-3 text-gold flex-shrink-0" />
|
||||
)}
|
||||
<span className="text-xs text-base-700 truncate">{cover.name}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-px">
|
||||
{data.sensors.map((sensor) => (
|
||||
<div
|
||||
key={sensor.entity_id}
|
||||
className="flex items-center justify-between px-3 py-2 bg-base-100 border-l-2 border-base-300 hover:border-iris transition-colors"
|
||||
>
|
||||
<span className="text-xs text-base-700">{sensor.name}</span>
|
||||
<span className="text-sm data-value text-base-900">
|
||||
{typeof sensor.state === "number"
|
||||
? sensor.state.toFixed(1)
|
||||
: sensor.state}
|
||||
<span className="text-[10px] text-base-500 ml-0.5">{sensor.unit}</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{cover.current_position != null && cover.current_position > 0 && (
|
||||
<span className="text-[10px] font-mono text-base-500 mr-1">
|
||||
{cover.current_position}%
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleControl(cover.entity_id, "open")}
|
||||
disabled={isPending}
|
||||
className="w-6 h-6 flex items-center justify-center bg-base-200 hover:bg-mint/20 hover:text-mint transition-colors"
|
||||
title="Öffnen"
|
||||
>
|
||||
<ArrowUp className="w-3 h-3" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleControl(cover.entity_id, "stop")}
|
||||
disabled={isPending}
|
||||
className="w-6 h-6 flex items-center justify-center bg-base-200 hover:bg-gold/20 hover:text-gold transition-colors"
|
||||
title="Stopp"
|
||||
>
|
||||
<Square className="w-2.5 h-2.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleControl(cover.entity_id, "close")}
|
||||
disabled={isPending}
|
||||
className="w-6 h-6 flex items-center justify-center bg-base-200 hover:bg-cherry/20 hover:text-cherry transition-colors"
|
||||
title="Schließen"
|
||||
>
|
||||
<ArrowDown className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Binary Sensors */}
|
||||
{data.binary_sensors.length > 0 && (
|
||||
<section>
|
||||
<div className="flex items-center gap-1.5 mb-3">
|
||||
<Eye className="w-3 h-3 text-base-500" />
|
||||
<span className="data-label">Sensoren</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-px bg-base-300">
|
||||
{data.binary_sensors.map((bs) => {
|
||||
const isActive = bs.state === "on";
|
||||
const Icon =
|
||||
bs.device_class === "door"
|
||||
? isActive
|
||||
? DoorOpen
|
||||
: DoorClosed
|
||||
: bs.device_class === "window"
|
||||
? isActive
|
||||
? DoorOpen
|
||||
: DoorClosed
|
||||
: Eye;
|
||||
return (
|
||||
<div
|
||||
key={bs.entity_id}
|
||||
className={`flex items-center gap-2 px-3 py-2 ${
|
||||
isActive ? "bg-gold/[0.06]" : "bg-base-50"
|
||||
}`}
|
||||
>
|
||||
<Icon
|
||||
className={`w-3 h-3 flex-shrink-0 ${
|
||||
isActive ? "text-gold" : "text-base-500"
|
||||
}`}
|
||||
/>
|
||||
<span className="text-[10px] text-base-600 truncate font-mono">
|
||||
{bs.name}
|
||||
</span>
|
||||
<div
|
||||
className={`w-1.5 h-1.5 flex-shrink-0 ml-auto ${
|
||||
isActive ? "bg-gold shadow-[0_0_6px_#e8a44a80]" : "bg-base-400"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -133,18 +133,34 @@ export const MOCK_DATA: DashboardData = {
|
|||
{ entity_id: "light.balkon", name: "Balkon", state: "off", brightness: 0 },
|
||||
{ entity_id: "light.gaestezimmer", name: "Gästezimmer", state: "off", brightness: 0 },
|
||||
],
|
||||
switches: [
|
||||
{ entity_id: "switch.buro_pc", name: "Büro PC", state: "on" },
|
||||
{ entity_id: "switch.teich", name: "Teich Socket", state: "on" },
|
||||
{ entity_id: "switch.kinderzimmer", name: "Kinderzimmer Plug", state: "off" },
|
||||
],
|
||||
covers: [
|
||||
{ entity_id: "cover.wohnzimmer", name: "Wohnzimmer Rollo", state: "open", position: 100 },
|
||||
{ entity_id: "cover.schlafzimmer", name: "Schlafzimmer Rollo", state: "closed", position: 0 },
|
||||
{ entity_id: "cover.kueche", name: "Küche Rollo", state: "open", position: 75 },
|
||||
{ entity_id: "cover.wohnzimmer", name: "Wohnzimmer Rollo", state: "open", current_position: 100, device_class: "shutter" },
|
||||
{ entity_id: "cover.schlafzimmer", name: "Schlafzimmer Rollo", state: "closed", current_position: 0, device_class: "shutter" },
|
||||
{ entity_id: "cover.kueche", name: "Küche Rollo", state: "open", current_position: 75, device_class: "curtain" },
|
||||
],
|
||||
sensors: [
|
||||
{ entity_id: "sensor.temp_wohnzimmer", name: "Wohnzimmer", state: 21.5, unit: "°C" },
|
||||
{ entity_id: "sensor.temp_aussen", name: "Außen", state: 7.8, unit: "°C" },
|
||||
{ entity_id: "sensor.temp_schlafzimmer", name: "Schlafzimmer", state: 19.2, unit: "°C" },
|
||||
{ entity_id: "sensor.temp_wohnzimmer", name: "Wohnzimmer", state: 21.5, unit: "°C", device_class: "temperature" },
|
||||
{ entity_id: "sensor.temp_aussen", name: "Außen", state: 7.8, unit: "°C", device_class: "temperature" },
|
||||
{ entity_id: "sensor.temp_schlafzimmer", name: "Schlafzimmer", state: 19.2, unit: "°C", device_class: "temperature" },
|
||||
{ entity_id: "sensor.hum_wohnzimmer", name: "Wohnzimmer", state: 48.2, unit: "%", device_class: "humidity" },
|
||||
{ entity_id: "sensor.hum_kueche", name: "Küche", state: 33.4, unit: "%", device_class: "humidity" },
|
||||
],
|
||||
binary_sensors: [
|
||||
{ entity_id: "binary_sensor.front_door", name: "Haustür", state: "off", device_class: "door" },
|
||||
{ entity_id: "binary_sensor.motion_og", name: "Bewegung OG", state: "off", device_class: "occupancy" },
|
||||
],
|
||||
climate: [
|
||||
{ entity_id: "climate.klima", name: "Klima", state: "heat", current_temperature: 23, target_temperature: 25, hvac_modes: ["off", "heat", "cool"], humidity: 28 },
|
||||
],
|
||||
lights_on: 3,
|
||||
lights_total: 8,
|
||||
switches_on: 2,
|
||||
switches_total: 3,
|
||||
},
|
||||
mqtt: {
|
||||
connected: true,
|
||||
|
|
|
|||
|
|
@ -95,7 +95,9 @@ export default function Dashboard() {
|
|||
{data.servers.servers.map((srv) => (
|
||||
<ServerCard key={srv.name} server={srv} />
|
||||
))}
|
||||
<HomeAssistant data={data.ha} />
|
||||
<div className="xl:col-span-2">
|
||||
<HomeAssistant data={data.ha} />
|
||||
</div>
|
||||
<TasksCard data={data.tasks} />
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue