mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
neatp
This commit is contained in:
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
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,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.'
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
Reference in New Issue
Block a user