mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -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-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>
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,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.'
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user