home assistant entities yay yay

This commit is contained in:
legop3
2026-09-16 12:22:41 -04:00
parent b48348d89e
commit f3e3b6a803
26 changed files with 646 additions and 5 deletions
@@ -0,0 +1,58 @@
import { useCallback } from 'react';
import * as FaIcons from 'react-icons/fa';
import { FaCube, FaLock } from 'react-icons/fa';
import { useSessionActions, useSessionSelector } from '../../context/SessionContext';
import CardFrame from '../CardFrame';
// Each control keeps its own JSX file; this panel only selects its renderer.
import ReadOnlyControl from './controls/ReadOnlyControl';
import ToggleControl from './controls/ToggleControl';
import NumberControl from './controls/NumberControl';
import TextControl from './controls/TextControl';
import SelectControl from './controls/SelectControl';
import ButtonControl from './controls/ButtonControl';
const controls = { readOnly: ReadOnlyControl, toggle: ToggleControl, number: NumberControl, text: TextControl, select: SelectControl, button: ButtonControl };
function ActivityTile({ entity, connected, allowed, admin }) {
const { homeAssistantActivityAct } = useSessionActions();
const onChange = useCallback((value) => homeAssistantActivityAct(entity.id, value), [homeAssistantActivityAct, entity.id]);
// Resolve precisely the same Font Awesome names accepted by social links.
// Unknown names remain usable with a neutral fallback rather than an error.
const candidate = FaIcons[entity.icon?.trim()];
const Icon = typeof candidate === 'function' ? candidate : FaCube;
const Control = controls[entity.type] || ReadOnlyControl;
const disabled = !connected || !entity.available || !allowed || (entity.locked && !admin);
return <div className={`flex min-w-0 flex-col gap-0.5 rounded border px-0.5 py-0.5 ${entity.locked ? 'border-amber-700/60 bg-amber-950/70' : 'border-neutral-700 bg-neutral-950'}`}>
<div className="-mx-0.5 -mt-0.5 flex min-h-5 min-w-0 items-center gap-0.5 rounded-t bg-black/45 px-0.5 py-0.5 text-white">
<Icon className="shrink-0 text-xs" aria-hidden="true" />
<span title={entity.name} className="min-w-0 flex-1 truncate text-[0.78rem] font-semibold leading-none">{entity.name}</span>
{entity.locked ? <span title={admin ? 'Locked for users; admins can still control' : 'Locked'} className="flex items-center gap-0.5 text-[0.65rem] text-amber-200"><FaLock aria-hidden="true" />Locked</span> : null}
</div>
{!entity.available && entity.type !== 'readOnly' ? <span className="text-xs text-amber-200">Unavailable</span> : null}
<Control entity={entity} disabled={disabled} onChange={onChange} />
</div>;
}
export default function HomeAssistantActivitiesPanel() {
const state = useSessionSelector((session) => session.session?.homeAssistantActivities);
const socketConnected = useSessionSelector((session) => session.connected);
const role = useSessionSelector((session) => session.session?.role);
const mode = useSessionSelector((session) => session.session?.mode);
const admin = role === 'admin' || role === 'lockdown';
const allowed = ['user', 'admin', 'lockdown'].includes(role) && (mode !== 'admin' || admin) && (mode !== 'lockdown' || role === 'lockdown');
if (!state?.enabled || !state.items.length) return null;
// Session data survives a browser disconnect. Disable immediately so local
// debounce timers are cancelled even when HA itself remains connected.
const connected = state.connected && socketConnected;
// Reuse the room-control auto-fit grid and compact tile rhythm, while keeping
// all data and commands in the Activities namespace on both device layouts.
return <CardFrame title="Home Assistant" bodyClassName="space-y-0.5 text-sm"
actions={<span className={`rounded px-1 py-0.5 text-xs font-semibold leading-none ${connected ? 'bg-emerald-900 text-emerald-100' : 'bg-amber-900 text-amber-100'}`}>{connected ? 'Connected' : 'Offline'}</span>}>
{!connected ? <p className="px-0.5 text-xs text-amber-200">{socketConnected ? 'Home Assistant is offline.' : 'Server disconnected.'} Values may be out of date.</p> : null}
{!allowed ? <p className="px-0.5 text-xs text-slate-400">Controls are read-only with your current access.</p> : null}
<div className="grid grid-cols-[repeat(auto-fit,minmax(min(9rem,100%),1fr))] gap-0.5">
{state.items.map((entity) => <ActivityTile key={entity.id} entity={entity} connected={connected} allowed={allowed} admin={admin} />)}
</div>
</CardFrame>;
}