home assistant lights and other tweaks

This commit is contained in:
legop3
2025-11-21 01:38:53 -05:00
parent e514b1b9e1
commit e2a3d78bc0
18 changed files with 497 additions and 63 deletions
+6 -5
View File
@@ -35,7 +35,7 @@ export default function DrivePanel() {
<div className="space-y-0.5">
<ActionCard
title="Start Driving"
description="Press to enable driving mode, then start moving. The headlamps should illuminate."
description="Press to enable driving mode, then start moving."
statuses={[{ label: drivingMode ? 'Ready!' : 'Not Ready!', active: drivingMode }]}
tone="emerald"
onClick={handleStartDrive}
@@ -43,7 +43,7 @@ export default function DrivePanel() {
/>
<ActionCard
title="Dock and Charge"
description="Line the rover up about a foot from the dock, then trigger an automatic approach."
description="Line the rover up about a foot from the dock, then trigger an automatic docking attempt."
statuses={[
{ label: docked ? 'Docked!' : 'Not Docked!', active: docked },
{ label: charging ? 'Charging!' : 'Not Charging!', active: charging },
@@ -71,12 +71,13 @@ function ActionCard({ title, description, statuses, tone, onClick, disabled, foo
>
<p className="text-base font-semibold">{title}</p>
<p className="text-sm text-white/90">{description}</p>
<div className="mt-0.5 flex flex-wrap gap-0.5">
{/* center statuses in button */}
<div className="mt-0.5 flex flex-wrap gap-0.5 items-center w-full justify-center">
{statuses.map((status) => (
<span
key={status.label}
className={`px-0.5 py-0.5 text-xs font-semibold ${
status.active ? 'bg-lime-300 text-emerald-900' : 'bg-emerald-900 text-emerald-100'
className={`p-1 text-xs font-semibold ${
status.active ? 'bg-green-600 text-white' : 'bg-red-500 text-white'
}`}
>
{status.label}
@@ -151,9 +151,13 @@ export default function GamepadMappingSettings() {
}, {});
}, []);
const getValueLabel = (action) => {
const getStored = (action) => {
const [group, key] = action.path;
const stored = mapping?.[group]?.[key] ?? null;
return mapping?.[group]?.[key] ?? null;
};
const getValueLabel = (action) => {
const stored = getStored(action);
return action.type === 'axis' ? formatAxis(stored) : formatButton(stored);
};
@@ -161,6 +165,17 @@ export default function GamepadMappingSettings() {
save((prev) => updatePath(prev, action.path, () => null));
};
const handleInvert = (action) => {
const stored = getStored(action);
if (!stored) return;
save((prev) =>
updatePath(prev, action.path, (current) => ({
...current,
invert: !current?.invert,
})),
);
};
return (
<section className="panel-section space-y-0.5 text-sm">
<div className="flex items-center justify-between">
@@ -232,6 +247,20 @@ export default function GamepadMappingSettings() {
<p className="text-[0.65rem] text-slate-400">{getValueLabel(action)}</p>
</div>
<div className="flex items-center gap-0.5">
{action.type === 'axis' && (
<button
type="button"
disabled={!getStored(action)}
onClick={() => handleInvert(action)}
className={`${
getStored(action)?.invert
? 'px-0.5 py-0.5 bg-amber-400 text-amber-900 hover:bg-amber-300'
: 'button-dark'
} text-[0.7rem] font-medium disabled:opacity-50 disabled:cursor-not-allowed`}
>
Invert
</button>
)}
<button type="button" onClick={() => handleClear(action)} className="button-dark text-[0.7rem]">
Clear
</button>
@@ -0,0 +1,97 @@
import { useMemo } from 'react';
import { useSession } from '../context/SessionContext.jsx';
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 (
<span className={`rounded px-1 py-0.5 text-xs font-semibold leading-tight ${styles}`}>
{label}
</span>
);
}
function EntityRow({ entity, connected, onToggle }) {
const unavailable = entity.state === 'unavailable' || !entity.available;
const isOn = entity.state === 'on';
const statusTone = unavailable ? 'warn' : isOn ? 'success' : 'muted';
const statusLabel = unavailable ? 'Unavailable' : isOn ? 'On' : 'Off';
const disableToggle = !connected || unavailable;
return (
<article className="flex items-center justify-between gap-1 rounded border border-slate-800 bg-zinc-950 px-1 py-0.5">
<div className="min-w-0">
<div className="flex items-center gap-1 text-sm text-white">
<span className="truncate font-semibold">{entity.name || entity.id}</span>
<StatusBadge label={entity.type === 'light' ? 'Light' : 'Switch'} />
</div>
<div className="flex items-center gap-1 text-xs text-slate-400">
<StatusBadge label={statusLabel} tone={statusTone} />
{!connected && <span className="text-amber-200"> · Offline</span>}
</div>
</div>
<div className="flex items-center gap-0.5">
<button
type="button"
className="button-dark whitespace-nowrap px-1 py-0.5 text-xs"
disabled={disableToggle}
onClick={() => onToggle(entity.id)}
>
{isOn ? 'Turn off' : 'Turn on'}
</button>
</div>
</article>
);
}
export default function HomeAssistantControls() {
const { session, homeAssistantToggle } = useSession();
const ha = session?.homeAssistant;
const entities = useMemo(() => ha?.entities || [], [ha?.entities]);
if (!ha?.enabled) {
return (
<section className="panel-section space-y-0.5 text-sm text-slate-400">
<p className="text-slate-300">Light Controls</p>
<p className="text-slate-500">Not configured on the server.</p>
</section>
);
}
if (entities.length === 0) {
return (
<section className="panel-section space-y-0.5 text-sm text-slate-400">
<p className="text-slate-300">Light Controls</p>
<p className="text-slate-500">No lights or switches configured.</p>
</section>
);
}
const connected = Boolean(ha?.connected);
return (
<section className="panel-section space-y-0.5 text-base">
<header className="flex items-center justify-between gap-0.5 text-sm text-slate-400">
<div className="flex items-center gap-1">
<p>Home Assistant</p>
<span className="text-xs text-slate-500">{entities.length}</span>
</div>
<StatusBadge label={connected ? 'Connected' : 'Offline'} tone={connected ? 'success' : 'warn'} />
</header>
<div className="space-y-0.5">
{entities.map((entity) => (
<EntityRow
key={entity.id}
entity={entity}
connected={connected}
onToggle={homeAssistantToggle}
/>
))}
</div>
</section>
);
}
+2 -10
View File
@@ -2,19 +2,11 @@ import TelemetryPanel from './TelemetryPanel.jsx';
import DrivePanel from './DrivePanel.jsx';
import CameraServoPanel from './CameraServoPanel.jsx';
import RoomCameraPanel from './RoomCameraPanel.jsx';
import HomeAssistantControls from './HomeAssistantControls.jsx';
import SettingsPanel from './SettingsPanel.jsx';
import HelpPanel from './HelpPanel.jsx';
import Tabs, { Tab, TabList, TabPanel, TabPanels } from './Tabs.jsx';
function RoomControlsPlaceholder() {
return (
<div className="panel-section space-y-0.5 text-sm">
<p className="text-slate-400">Room controls</p>
<p className="text-slate-200">Coming soon: lighting, docks, and environmental toggles.</p>
</div>
);
}
export default function RightPaneTabs({ layout }) {
return (
<section className="panel text-base">
@@ -37,7 +29,7 @@ export default function RightPaneTabs({ layout }) {
<TabPanel id="room">
<div className="space-y-0.5">
<RoomCameraPanel defaultOrientation="vertical" />
<RoomControlsPlaceholder />
<HomeAssistantControls />
</div>
</TabPanel>
<TabPanel id="settings">