import { useMemo, useState } from 'react';
function normalizeState(value) {
return String(value || '').trim();
}
function humanizeUiState(value) {
const state = normalizeState(value);
if (!state) return '--';
if (state.includes('DOCKINGRUNNING')) return 'Returning';
if (state.includes('PAUSED')) return 'Paused';
if (state.includes('HOUSECLEANINGRUNNING')) return 'Cleaning House';
if (state.includes('SPOTCLEANINGRUNNING')) return 'Spot Cleaning';
if (state.includes('STATE_START')) return 'Starting';
if (state.includes('STATE_IDLE')) return 'Idle';
if (state.includes('STATE_STANDBY')) return 'Standby';
return state;
}
function statusToneClass(tone) {
if (tone === 'good') return 'border-emerald-200 bg-emerald-600 text-white';
if (tone === 'warn') return 'border-amber-200 bg-amber-600 text-white';
if (tone === 'danger') return 'border-rose-200 bg-rose-600 text-white';
if (tone === 'info') return 'border-sky-200 bg-sky-600 text-white';
return 'border-slate-200/30 bg-slate-700 text-slate-100';
}
function StatusRow({ label, value, tone = 'muted' }) {
return (
{label}
{value}
);
}
function MetricCard({ label, value, tone = 'muted' }) {
return (
);
}
function ActionPill({ label, tone = 'indigo' }) {
const toneClasses =
tone === 'emerald'
? 'border-emerald-200/70 bg-emerald-600/70 text-emerald-50'
: tone === 'amber'
? 'border-amber-200/70 bg-amber-600/70 text-amber-50'
: 'border-indigo-200/70 bg-indigo-600/70 text-indigo-50';
return (
{label}
);
}
export default function VipNeatoCard({ neato, onStart, onSendHome, onLocate, fullWidth = false }) {
const [working, setWorking] = useState('');
const [message, setMessage] = useState('');
const wrapClass = fullWidth ? 'w-full' : 'w-full max-w-xl';
const configured = Boolean(neato?.configured);
const connected = Boolean(neato?.enabled && neato?.connected);
const docked = Boolean(neato?.telemetry?.extPowerPresent);
const charging = Boolean(neato?.telemetry?.chargingActive);
const uiStateLabel = humanizeUiState(neato?.telemetry?.uiState);
const battery = neato?.telemetry?.batteryPercent;
const batteryLabel = Number.isFinite(battery) ? `${battery}%` : '--';
const voltage = neato?.telemetry?.batteryVoltage;
const voltageLabel = Number.isFinite(voltage) ? `${voltage.toFixed(2)} V` : '--';
const robotError = normalizeState(neato?.telemetry?.robotError) || '--';
const robotAlert = normalizeState(neato?.telemetry?.robotAlert) || '--';
const hasError = robotError !== '--' && !/^no errors$/i.test(robotError) && !/^200/.test(robotError);
const hasAlert = robotAlert !== '--' && !/^200/.test(robotAlert);
const controls = neato?.controls || {};
const canStart = Boolean(controls?.start?.available);
const canSendHome = Boolean(controls?.sendHome?.available);
const canLocate = Boolean(controls?.locate?.available);
const primaryAction = useMemo(() => {
if (docked) {
return {
key: 'start',
label: 'Start Cleaning',
pending: 'Starting...',
tone: 'emerald',
canRun: canStart,
fn: onStart,
successMessage: 'Start cleaning command sent.',
};
}
return {
key: 'home',
label: 'Send to Dock',
pending: 'Sending...',
tone: 'indigo',
canRun: canSendHome,
fn: onSendHome,
successMessage: 'Send to dock command sent.',
};
}, [docked, canStart, onStart, canSendHome, onSendHome]);
const canRunPrimary = configured && connected && primaryAction.canRun;
const canRunLocate = configured && connected && canLocate;
const runAction = async (key, fn, successMessage) => {
if (!fn) return;
setWorking(key);
setMessage('');
try {
await fn();
setMessage(successMessage);
} catch (err) {
setMessage(err?.message || 'Action failed.');
} finally {
setWorking('');
}
};
const statusText = !configured
? 'Not configured'
: !connected
? 'Offline'
: 'Online';
const statusTone = !configured ? 'warn' : !connected ? 'danger' : 'good';
const batteryTone =
battery == null ? 'muted' : battery >= 60 ? 'good' : battery >= 25 ? 'warn' : 'danger';
return (
Neato Control Surface (Gen3)
{docked ? 'Docked State' : 'Undocked State'}
This Neato tile mirrors rover mode controls: one context-aware primary action based on dock state.
runAction(primaryAction.key, primaryAction.fn, primaryAction.successMessage)}
className={`w-full rounded-xl border-2 px-1 py-1 text-center text-base font-semibold text-white transition disabled:opacity-50 ${
primaryAction.tone === 'emerald'
? 'border-emerald-200/70 bg-emerald-700 hover:bg-emerald-600'
: 'border-indigo-200/70 bg-indigo-700 hover:bg-indigo-600'
}`}
>
{working === primaryAction.key ? primaryAction.pending : primaryAction.label}
runAction('locate', onLocate, 'Play sound command sent.')}
className="w-full rounded-xl border-2 border-fuchsia-200/70 bg-fuchsia-700 px-1 py-0.75 text-center text-sm font-semibold text-fuchsia-50 transition hover:bg-fuchsia-600 disabled:opacity-50"
>
{working === 'locate' ? 'Playing...' : 'Play Sound'}
{!configured || !connected || !primaryAction.canRun || !canLocate ? (
{!configured
? 'Set homeAssistant.neato.device in server config to enable Neato controls.'
: !connected
? 'Home Assistant is offline.'
: 'Waiting for required Neato entities in Home Assistant.'}
) : null}
{message ?
{message}
: null}
);
}