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-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-EI4GBqvV.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-IQlwhkNm.css">
<script type="module" crossorigin src="/assets/index-wLjyvnCl.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-pfHsbpoo.css">
</head>
<body>
<div id="root"></div>
+22
View File
@@ -34,6 +34,7 @@ const ENTITY_IDS = {
start: entityId('button', 'house_clean'),
sendHome: entityId('button', 'send_to_base'),
locate: entityId('button', 'locate_robot'),
clearErrors: entityId('button', 'clear_errors'),
},
sensors: {
batteryPercent: entityId('sensor', 'fuel_percent'),
@@ -92,6 +93,10 @@ function buildState() {
entityId: 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));
@@ -179,6 +184,10 @@ async function locateRobot() {
await pressButton(ENTITY_IDS.buttons.locate, 'locate');
}
async function clearErrors() {
await pressButton(ENTITY_IDS.buttons.clearErrors, 'clear_errors');
}
function getState() {
cachedState = buildState();
return cachedState;
@@ -220,6 +229,18 @@ io.on('connection', (socket) => {
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();
@@ -229,5 +250,6 @@ module.exports = {
startCleaning,
sendHome,
locateRobot,
clearErrors,
neatoEvents: events,
};
+2
View File
@@ -22,6 +22,7 @@ export default function VipPanel() {
neatoStart,
neatoSendHome,
neatoLocate,
neatoClearErrors,
} = useSession();
const { value: identity, save: saveIdentity } = useSettingsNamespace('identity', { cookieUserId: '' });
const { value: profile } = useSettingsNamespace('profile', { nickname: '' });
@@ -80,6 +81,7 @@ export default function VipPanel() {
onStart={neatoStart}
onSendHome={neatoSendHome}
onLocate={neatoLocate}
onClearErrors={neatoClearErrors}
fullWidth
/>
<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 [message, setMessage] = useState('');
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 canSendHome = Boolean(controls?.sendHome?.available);
const canLocate = Boolean(controls?.locate?.available);
const canClearErrors = Boolean(controls?.clearErrors?.available);
const primaryAction = useMemo(() => {
if (docked) {
@@ -94,6 +102,7 @@ export default function VipNeatoCard({ neato, onStart, onSendHome, onLocate, ful
const canRunPrimary = configured && connected && primaryAction.canRun;
const canRunLocate = configured && connected && canLocate;
const canRunClearErrors = configured && connected && canClearErrors;
const runAction = async (key, fn, successMessage) => {
if (!fn) return;
@@ -120,8 +129,9 @@ export default function VipNeatoCard({ neato, onStart, onSendHome, onLocate, ful
return (
<section className={`surface text-sm text-slate-200 ${wrapClass}`}>
<div className="grid gap-0.5">
<div className="flex items-center justify-between gap-0.5">
<p className="text-sm text-slate-100">Neato</p>
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-0.5">
<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)}`}>
{headerStatus}
</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-base font-semibold text-slate-100">{primaryState}</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
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
type="button"
disabled={!canRunLocate || Boolean(working)}
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 === 'locate' ? 'Playing...' : 'Play sound'}
</button>
<div className="grid grid-cols-2 gap-0.5">
<button
type="button"
disabled={!canRunLocate || Boolean(working)}
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 === 'locate' ? 'Playing...' : 'Play sound'}
</button>
<button
type="button"
disabled={!canRunClearErrors || Boolean(working)}
onClick={() => runAction('clearErrors', onClearErrors, 'Clear errors command sent.')}
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 === 'clearErrors' ? 'Clearing...' : 'Clear errors'}
</button>
</div>
</div>
</div>
</section>
@@ -166,14 +187,14 @@ export default function VipNeatoCard({ neato, onStart, onSendHome, onLocate, ful
</div>
<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="Robot error" value={robotError} tone={hasError ? 'danger' : 'muted'} />
<Row label="Robot alert" value={robotAlert} tone={hasAlert ? 'warn' : 'muted'} />
</div>
</section>
{!configured || !connected || !primaryAction.canRun || !canLocate ? (
{!configured || !connected || !primaryAction.canRun || !canLocate || !canClearErrors ? (
<p className="text-xs text-slate-400 text-center">
{!configured
? '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'),
neatoSendHome: () => emitWithAck('neato:sendHome'),
neatoLocate: () => emitWithAck('neato:locate'),
neatoClearErrors: () => emitWithAck('neato:clearErrors'),
setNickname: (nickname) => emitWithAck('nickname:set', { nickname }),
requestVerification: () => emitWithAck('verification:request'),
requestPrivateRoverAccess: (roverId) =>