// Home Assistant Controls // Purpose: Defines the Home Assistant Controls module and the local helpers/components used in this file. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. import { useEffect, useMemo, useRef, useState } from 'react'; import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx'; import { useControlSystem } from '../../controls/index.js'; import { formatKeyLabel } from '../../controls/keymapUtils.js'; function StatusBadge({ label, tone = 'muted' }) { const styles = tone === 'success' ? 'bg-emerald-900 text-emerald-100' : tone === 'warn' ? 'bg-amber-900 text-amber-100' : 'bg-slate-800 text-slate-200'; return ( {label} ); } function clampHue(value) { if (!Number.isFinite(value)) return 0; const wrapped = value % 360; return wrapped < 0 ? wrapped + 360 : wrapped; } function rgbToHue(rgb) { if (!Array.isArray(rgb) || rgb.length < 3) return 0; const [rRaw, gRaw, bRaw] = rgb; const r = Math.max(0, Math.min(255, Number(rRaw))) / 255; const g = Math.max(0, Math.min(255, Number(gRaw))) / 255; const b = Math.max(0, Math.min(255, Number(bRaw))) / 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); const delta = max - min; if (delta === 0) return 0; let hue = 0; if (max === r) { hue = ((g - b) / delta) % 6; } else if (max === g) { hue = (b - r) / delta + 2; } else { hue = (r - g) / delta + 4; } return clampHue(Math.round(hue * 60)); } function hueToRgb(hue) { const h = clampHue(hue); const c = 1; const x = 1 - Math.abs(((h / 60) % 2) - 1); let r = 0; let g = 0; let b = 0; if (h < 60) { r = c; g = x; } else if (h < 120) { r = x; g = c; } else if (h < 180) { g = c; b = x; } else if (h < 240) { g = x; b = c; } else if (h < 300) { r = x; b = c; } else { r = c; b = x; } return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; } function getEntityHue(entity) { if (!entity) return 0; if (Array.isArray(entity.hsColor)) { return clampHue(Number(entity.hsColor[0])); } if (Array.isArray(entity.rgbColor)) { return rgbToHue(entity.rgbColor); } return 0; } function EntityRow({ entity, connected, controlsLocked, onToggle, onSetColor, onSetWhite }) { const unavailable = entity.state === 'unavailable' || !entity.available; const isOn = entity.state === 'on'; const supportsColor = entity.type === 'light' && entity.supportsColor; const statusTone = unavailable ? 'warn' : isOn ? 'success' : 'muted'; const statusLabel = unavailable ? 'Unavailable' : isOn ? 'On' : 'Off'; const disableToggle = controlsLocked || !connected || unavailable; const disableColor = disableToggle || !supportsColor; const [hue, setHue] = useState(() => getEntityHue(entity)); const hueRef = useRef(hue); const draggingRef = useRef(false); const toneStyles = unavailable ? 'border-slate-800 bg-slate-900 text-slate-400 cursor-not-allowed' : controlsLocked ? 'border-slate-800 bg-slate-900 text-slate-300 cursor-not-allowed' : isOn ? 'border-emerald-700 bg-emerald-900/80 text-emerald-50 hover:bg-emerald-800' : 'border-rose-800 bg-rose-900/80 text-rose-50 hover:bg-rose-800'; useEffect(() => { if (!supportsColor || draggingRef.current) return; const nextHue = getEntityHue(entity); hueRef.current = nextHue; setHue(nextHue); }, [entity.rgbColor, entity.hsColor, supportsColor]); const handleHueChange = (event) => { const nextHue = clampHue(Number(event.target.value)); hueRef.current = nextHue; setHue(nextHue); }; const commitHue = () => { if (disableColor || !onSetColor) return; onSetColor(entity.id, hueToRgb(hueRef.current)); }; const stopPropagation = (event) => { event.stopPropagation(); }; const handleSetWhite = (event) => { stopPropagation(event); if (disableColor || !onSetWhite) return; onSetWhite(entity.id); }; const displayRgb = supportsColor ? hueToRgb(hueRef.current) : [255, 255, 255]; const displayColor = `rgb(${displayRgb.join(',')})`; return ( ); } export default function HomeAssistantControls() { const { state: { keymap }, } = useControlSystem(); const ha = useSessionSelector((state) => state.session?.homeAssistant || null); const { homeAssistantToggle, homeAssistantSetLightColor, homeAssistantSetLightWhite } = useSessionActions(); const entities = useMemo(() => ha?.entities || [], [ha?.entities]); const lightPolicy = ha?.lightPolicy || null; const controlsLocked = Boolean(lightPolicy?.locked || lightPolicy?.lockedOn); const lockState = lightPolicy?.lockState || (lightPolicy?.lockedOn ? 'on' : null); const onKeyLabel = formatKeyLabel(keymap?.homeAssistantOn?.[0]); const offKeyLabel = formatKeyLabel(keymap?.homeAssistantOff?.[0]); if (!ha?.enabled) { return (

Room Controls

Not configured on the server.

); } if (entities.length === 0) { return (

Room Controls

No lights or switches configured.

); } const connected = Boolean(ha?.connected); return (

Room Controls

{entities.length}
On {onKeyLabel ? : null} Off {offKeyLabel ? : null}
{controlsLocked ? : null}
{controlsLocked ? (

{lockState === 'off' ? 'Lights are locked off. Room controls are disabled.' : 'Lights are locked on. Room controls are disabled.'}

) : null}
{entities.map((entity) => ( ))}
); } function KeyPill({ label }) { if (!label) return null; return {label}; }