This commit is contained in:
legop3
2026-04-26 21:09:11 -04:00
parent c28cb4bb20
commit 304c984423
9 changed files with 196 additions and 150 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -11,8 +11,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" /> <meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title> <title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-EI4GBqvV.js"></script> <script type="module" crossorigin src="/assets/index-wLjyvnCl.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-IQlwhkNm.css"> <link rel="stylesheet" crossorigin href="/assets/index-pfHsbpoo.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+22
View File
@@ -34,6 +34,7 @@ const ENTITY_IDS = {
start: entityId('button', 'house_clean'), start: entityId('button', 'house_clean'),
sendHome: entityId('button', 'send_to_base'), sendHome: entityId('button', 'send_to_base'),
locate: entityId('button', 'locate_robot'), locate: entityId('button', 'locate_robot'),
clearErrors: entityId('button', 'clear_errors'),
}, },
sensors: { sensors: {
batteryPercent: entityId('sensor', 'fuel_percent'), batteryPercent: entityId('sensor', 'fuel_percent'),
@@ -92,6 +93,10 @@ function buildState() {
entityId: ENTITY_IDS.buttons.locate, entityId: ENTITY_IDS.buttons.locate,
available: hasEntity(ENTITY_IDS.buttons.locate), available: hasEntity(ENTITY_IDS.buttons.locate),
}, },
clearErrors: {
entityId: ENTITY_IDS.buttons.clearErrors,
available: hasEntity(ENTITY_IDS.buttons.clearErrors),
},
}; };
const batteryPercentValue = parseNumber(readState(ENTITY_IDS.sensors.batteryPercent)); const batteryPercentValue = parseNumber(readState(ENTITY_IDS.sensors.batteryPercent));
@@ -179,6 +184,10 @@ async function locateRobot() {
await pressButton(ENTITY_IDS.buttons.locate, 'locate'); await pressButton(ENTITY_IDS.buttons.locate, 'locate');
} }
async function clearErrors() {
await pressButton(ENTITY_IDS.buttons.clearErrors, 'clear_errors');
}
function getState() { function getState() {
cachedState = buildState(); cachedState = buildState();
return cachedState; return cachedState;
@@ -220,6 +229,18 @@ io.on('connection', (socket) => {
cb({ error: err.message }); cb({ error: err.message });
} }
}); });
socket.on('neato:clearErrors', async (_, cb = () => {}) => {
try {
if (!isVerified(socket)) {
throw new Error('VIP verification required');
}
await clearErrors();
cb({ success: true });
} catch (err) {
cb({ error: err.message });
}
});
}); });
emitUpdate(); emitUpdate();
@@ -229,5 +250,6 @@ module.exports = {
startCleaning, startCleaning,
sendHome, sendHome,
locateRobot, locateRobot,
clearErrors,
neatoEvents: events, neatoEvents: events,
}; };
+2
View File
@@ -22,6 +22,7 @@ export default function VipPanel() {
neatoStart, neatoStart,
neatoSendHome, neatoSendHome,
neatoLocate, neatoLocate,
neatoClearErrors,
} = useSession(); } = useSession();
const { value: identity, save: saveIdentity } = useSettingsNamespace('identity', { cookieUserId: '' }); const { value: identity, save: saveIdentity } = useSettingsNamespace('identity', { cookieUserId: '' });
const { value: profile } = useSettingsNamespace('profile', { nickname: '' }); const { value: profile } = useSettingsNamespace('profile', { nickname: '' });
@@ -80,6 +81,7 @@ export default function VipPanel() {
onStart={neatoStart} onStart={neatoStart}
onSendHome={neatoSendHome} onSendHome={neatoSendHome}
onLocate={neatoLocate} onLocate={neatoLocate}
onClearErrors={neatoClearErrors}
fullWidth fullWidth
/> />
<VipAudioUploadCard <VipAudioUploadCard
+43 -22
View File
@@ -43,7 +43,14 @@ function Row({ label, value, tone = 'muted' }) {
); );
} }
export default function VipNeatoCard({ neato, onStart, onSendHome, onLocate, fullWidth = false }) { export default function VipNeatoCard({
neato,
onStart,
onSendHome,
onLocate,
onClearErrors,
fullWidth = false,
}) {
const [working, setWorking] = useState(''); const [working, setWorking] = useState('');
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const wrapClass = fullWidth ? 'w-full' : 'w-full max-w-xl'; const wrapClass = fullWidth ? 'w-full' : 'w-full max-w-xl';
@@ -68,6 +75,7 @@ export default function VipNeatoCard({ neato, onStart, onSendHome, onLocate, ful
const canStart = Boolean(controls?.start?.available); const canStart = Boolean(controls?.start?.available);
const canSendHome = Boolean(controls?.sendHome?.available); const canSendHome = Boolean(controls?.sendHome?.available);
const canLocate = Boolean(controls?.locate?.available); const canLocate = Boolean(controls?.locate?.available);
const canClearErrors = Boolean(controls?.clearErrors?.available);
const primaryAction = useMemo(() => { const primaryAction = useMemo(() => {
if (docked) { if (docked) {
@@ -94,6 +102,7 @@ export default function VipNeatoCard({ neato, onStart, onSendHome, onLocate, ful
const canRunPrimary = configured && connected && primaryAction.canRun; const canRunPrimary = configured && connected && primaryAction.canRun;
const canRunLocate = configured && connected && canLocate; const canRunLocate = configured && connected && canLocate;
const canRunClearErrors = configured && connected && canClearErrors;
const runAction = async (key, fn, successMessage) => { const runAction = async (key, fn, successMessage) => {
if (!fn) return; if (!fn) return;
@@ -120,8 +129,9 @@ export default function VipNeatoCard({ neato, onStart, onSendHome, onLocate, ful
return ( return (
<section className={`surface text-sm text-slate-200 ${wrapClass}`}> <section className={`surface text-sm text-slate-200 ${wrapClass}`}>
<div className="grid gap-0.5"> <div className="grid gap-0.5">
<div className="flex items-center justify-between gap-0.5"> <div className="grid grid-cols-[1fr_auto_1fr] items-center gap-0.5">
<p className="text-sm text-slate-100">Neato</p> <div />
<p className="text-sm text-slate-100 text-center">Neato</p>
<span className={`rounded px-1 py-0.25 text-xs font-semibold ${metricToneClass(headerTone)}`}> <span className={`rounded px-1 py-0.25 text-xs font-semibold ${metricToneClass(headerTone)}`}>
{headerStatus} {headerStatus}
</span> </span>
@@ -134,24 +144,35 @@ export default function VipNeatoCard({ neato, onStart, onSendHome, onLocate, ful
<div className="text-xs text-slate-300">State</div> <div className="text-xs text-slate-300">State</div>
<div className="text-base font-semibold text-slate-100">{primaryState}</div> <div className="text-base font-semibold text-slate-100">{primaryState}</div>
</div> </div>
<div className="grid grid-cols-2 gap-0.5">
<button
type="button"
disabled={!canRunPrimary || Boolean(working)}
onClick={() => runAction(primaryAction.key, primaryAction.fn, primaryAction.successMessage)}
className={`rounded-md px-1 py-1 text-base font-semibold transition disabled:opacity-50 ${primaryAction.toneClass}`}
>
{working === primaryAction.key ? primaryAction.pending : primaryAction.label}
</button>
<button <div className="grid grid-cols-2 gap-0.5">
type="button" <button
disabled={!canRunPrimary || Boolean(working)} type="button"
onClick={() => runAction(primaryAction.key, primaryAction.fn, primaryAction.successMessage)} disabled={!canRunLocate || Boolean(working)}
className={`rounded-md px-1 py-1 text-base font-semibold transition disabled:opacity-50 ${primaryAction.toneClass}`} onClick={() => runAction('locate', onLocate, 'Play sound command sent.')}
> className="rounded-md bg-fuchsia-600 px-1 py-0.75 text-sm font-semibold text-white transition hover:bg-fuchsia-500 disabled:opacity-50"
{working === primaryAction.key ? primaryAction.pending : primaryAction.label} >
</button> {working === 'locate' ? 'Playing...' : 'Play sound'}
</button>
<button <button
type="button" type="button"
disabled={!canRunLocate || Boolean(working)} disabled={!canRunClearErrors || Boolean(working)}
onClick={() => runAction('locate', onLocate, 'Play sound command sent.')} onClick={() => runAction('clearErrors', onClearErrors, 'Clear errors command sent.')}
className="rounded-md bg-fuchsia-600 px-1 py-0.75 text-sm font-semibold text-white transition hover:bg-fuchsia-500 disabled:opacity-50" className="rounded-md bg-amber-500 px-1 py-0.75 text-sm font-semibold text-slate-900 transition hover:bg-amber-400 disabled:opacity-50"
> >
{working === 'locate' ? 'Playing...' : 'Play sound'} {working === 'clearErrors' ? 'Clearing...' : 'Clear errors'}
</button> </button>
</div>
</div>
</div> </div>
</section> </section>
@@ -166,14 +187,14 @@ export default function VipNeatoCard({ neato, onStart, onSendHome, onLocate, ful
</div> </div>
<section className="surface-muted px-0.5 py-0.5"> <section className="surface-muted px-0.5 py-0.5">
<div className="grid gap-0.5"> <div className="grid gap-0.5 grid-cols-1 md:grid-cols-3">
<Row label="UI state" value={uiStateLabel} tone="muted" /> <Row label="UI state" value={uiStateLabel} tone="muted" />
<Row label="Robot error" value={robotError} tone={hasError ? 'danger' : 'muted'} /> <Row label="Robot error" value={robotError} tone={hasError ? 'danger' : 'muted'} />
<Row label="Robot alert" value={robotAlert} tone={hasAlert ? 'warn' : 'muted'} /> <Row label="Robot alert" value={robotAlert} tone={hasAlert ? 'warn' : 'muted'} />
</div> </div>
</section> </section>
{!configured || !connected || !primaryAction.canRun || !canLocate ? ( {!configured || !connected || !primaryAction.canRun || !canLocate || !canClearErrors ? (
<p className="text-xs text-slate-400 text-center"> <p className="text-xs text-slate-400 text-center">
{!configured {!configured
? 'Set homeAssistant.neato.device in server config to enable Neato controls.' ? 'Set homeAssistant.neato.device in server config to enable Neato controls.'
+1
View File
@@ -166,6 +166,7 @@ export function SessionProvider({ children }) {
neatoStart: () => emitWithAck('neato:start'), neatoStart: () => emitWithAck('neato:start'),
neatoSendHome: () => emitWithAck('neato:sendHome'), neatoSendHome: () => emitWithAck('neato:sendHome'),
neatoLocate: () => emitWithAck('neato:locate'), neatoLocate: () => emitWithAck('neato:locate'),
neatoClearErrors: () => emitWithAck('neato:clearErrors'),
setNickname: (nickname) => emitWithAck('nickname:set', { nickname }), setNickname: (nickname) => emitWithAck('nickname:set', { nickname }),
requestVerification: () => emitWithAck('verification:request'), requestVerification: () => emitWithAck('verification:request'),
requestPrivateRoverAccess: (roverId) => requestPrivateRoverAccess: (roverId) =>