/** A `cover.*` device card ("Rollo"). Real covers report their own movement and * position, so this just reflects HA's state - no client-side movement animation like * the design mockup used (it had no real backend to poll). */ import type { HaDevice, HaEntityState } from "../api/types"; import { SHUTTER_PRESETS, closedLabelFor, closedPercentFromPosition, positionFromClosedPercent, } from "../lib/shutter"; //: Home Assistant's `CoverEntityFeature.SET_POSITION` bit. const SUPPORT_SET_POSITION = 4; interface Props { device: HaDevice; state: HaEntityState | undefined; callService: (domain: string, service: string, body: Record) => Promise; optimistic: (entityId: string, patch: Partial) => void; onManualChange: () => void; } export function ShutterCard({ device, state, callService, optimistic, onManualChange }: Props) { const position = state?.attributes.current_position as number | undefined; const supportedFeatures = (state?.attributes.supported_features as number | undefined) ?? 0; const supportsPosition = position != null || (supportedFeatures & SUPPORT_SET_POSITION) !== 0; const closedPercent = closedPercentFromPosition(position ?? 0); const moving = state?.state === "opening" ? "up" : state?.state === "closing" ? "down" : null; const statusLabel = moving === "down" ? "Fährt runter …" : moving === "up" ? "Fährt hoch …" : closedLabelFor(closedPercent); const setPosition = (target: number) => { onManualChange(); optimistic(device.entity_id, { attributes: { ...state?.attributes, current_position: target }, }); void callService("cover", "set_cover_position", { entity_id: device.entity_id, position: target, }); }; const open = () => { onManualChange(); void callService("cover", "open_cover", { entity_id: device.entity_id }); }; const close = () => { onManualChange(); void callService("cover", "close_cover", { entity_id: device.entity_id }); }; const stop = () => { onManualChange(); void callService("cover", "stop_cover", { entity_id: device.entity_id }); }; return (
{device.name ?? "Rollo"}
{statusLabel}
{supportsPosition && (
{SHUTTER_PRESETS.map((preset) => { const active = closedLabelFor(closedPercent) === preset.label; return ( ); })}
)}
); } function BlindsIcon() { return ( ); }