betterneato

This commit is contained in:
legop3
2026-05-01 02:35:58 -04:00
parent 02d5d6ca13
commit 6523ea7417
8 changed files with 59 additions and 13 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
+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-DIzOCfJB.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CQrwYoqn.css">
<script type="module" crossorigin src="/assets/index-CKRKhxHH.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-EE3dCtON.css">
</head>
<body>
<div id="root"></div>
+32
View File
@@ -26,6 +26,7 @@ function normalizeDeviceName(value) {
}
const device = normalizeDeviceName(neatoConfig.device);
const RESUME_DELAY_MS = 3000;
function entityId(domain, suffix) {
if (!device) return '';
@@ -35,9 +36,11 @@ function entityId(domain, suffix) {
const ENTITY_IDS = {
buttons: {
start: entityId('button', 'house_clean'),
resume: entityId('button', 'resume_cleaning'),
sendHome: entityId('button', 'send_to_base'),
locate: entityId('button', 'locate_robot'),
clearErrors: entityId('button', 'clear_errors'),
powerCycle: entityId('button', 'powercycle'),
},
sensors: {
batteryPercent: entityId('sensor', 'fuel_percent'),
@@ -90,9 +93,11 @@ function isEntityAvailable(entityIdValue) {
function requiredEntityIds() {
return [
ENTITY_IDS.buttons.start,
ENTITY_IDS.buttons.resume,
ENTITY_IDS.buttons.sendHome,
ENTITY_IDS.buttons.locate,
ENTITY_IDS.buttons.clearErrors,
ENTITY_IDS.buttons.powerCycle,
ENTITY_IDS.sensors.batteryPercent,
ENTITY_IDS.sensors.batteryVoltage,
ENTITY_IDS.binarySensors.chargingActive,
@@ -117,6 +122,10 @@ function buildState() {
entityId: ENTITY_IDS.buttons.start,
available: hasEntity(ENTITY_IDS.buttons.start),
},
resume: {
entityId: ENTITY_IDS.buttons.resume,
available: hasEntity(ENTITY_IDS.buttons.resume),
},
sendHome: {
entityId: ENTITY_IDS.buttons.sendHome,
available: hasEntity(ENTITY_IDS.buttons.sendHome),
@@ -129,6 +138,10 @@ function buildState() {
entityId: ENTITY_IDS.buttons.clearErrors,
available: hasEntity(ENTITY_IDS.buttons.clearErrors),
},
powerCycle: {
entityId: ENTITY_IDS.buttons.powerCycle,
available: hasEntity(ENTITY_IDS.buttons.powerCycle),
},
};
const batteryPercentValue = parseNumber(readState(ENTITY_IDS.sensors.batteryPercent));
@@ -208,6 +221,8 @@ async function pressButton(entityIdValue, actionLabel) {
async function startCleaning() {
await pressButton(ENTITY_IDS.buttons.start, 'start');
await new Promise((resolve) => setTimeout(resolve, RESUME_DELAY_MS));
await pressButton(ENTITY_IDS.buttons.resume, 'resume_cleaning');
}
async function sendHome() {
@@ -222,6 +237,10 @@ async function clearErrors() {
await pressButton(ENTITY_IDS.buttons.clearErrors, 'clear_errors');
}
async function powerCycle() {
await pressButton(ENTITY_IDS.buttons.powerCycle, 'powercucle');
}
function getState() {
cachedState = buildState();
return cachedState;
@@ -275,6 +294,18 @@ io.on('connection', (socket) => {
cb({ error: err.message });
}
});
socket.on('neato:powerCycle', async (_, cb = () => {}) => {
try {
if (!isVerified(socket)) {
throw new Error('VIP verification required');
}
await powerCycle();
cb({ success: true });
} catch (err) {
cb({ error: err.message });
}
});
});
emitUpdate();
@@ -285,5 +316,6 @@ module.exports = {
sendHome,
locateRobot,
clearErrors,
powerCycle,
neatoEvents: events,
};
+2
View File
@@ -27,6 +27,7 @@ export default function VipPanel() {
neatoSendHome,
neatoLocate,
neatoClearErrors,
neatoPowerCycle,
liftUp,
liftDown,
} = useSession();
@@ -81,6 +82,7 @@ export default function VipPanel() {
onSendHome={neatoSendHome}
onLocate={neatoLocate}
onClearErrors={neatoClearErrors}
onPowerCycle={neatoPowerCycle}
fullWidth
/>
<VipLiftCard
+13 -2
View File
@@ -44,6 +44,7 @@ export default function VipNeatoCard({
onSendHome,
onLocate,
onClearErrors,
onPowerCycle,
fullWidth = false,
}) {
const [working, setWorking] = useState('');
@@ -71,11 +72,13 @@ export default function VipNeatoCard({
const canSendHome = Boolean(controls?.sendHome?.available);
const canLocate = Boolean(controls?.locate?.available);
const canClearErrors = Boolean(controls?.clearErrors?.available);
const canPowerCycle = Boolean(controls?.powerCycle?.available);
const canRunStart = configured && connected && canStart;
const canRunSendHome = configured && connected && canSendHome;
const canRunLocate = configured && connected && canLocate;
const canRunClearErrors = configured && connected && canClearErrors;
const canRunPowerCycle = configured && connected && canPowerCycle;
const runAction = async (key, fn) => {
if (!fn) return;
@@ -131,7 +134,7 @@ export default function VipNeatoCard({
>
{working === 'sendHome' ? 'Sending...' : 'Send to dock'}
</button>
<div className="grid grid-cols-2 gap-0.5">
<div className="grid grid-cols-3 gap-0.5">
<button
type="button"
disabled={!canRunLocate || Boolean(working)}
@@ -148,6 +151,14 @@ export default function VipNeatoCard({
>
{working === 'clearErrors' ? 'Clearing...' : 'Clear errors'}
</button>
<button
type="button"
disabled={!canRunPowerCycle || Boolean(working)}
onClick={() => runAction('powerCycle', onPowerCycle)}
className="w-full rounded-md border border-rose-300 bg-rose-600 px-1 py-0.5 text-xs font-semibold text-white transition hover:border-rose-500 hover:bg-rose-500 disabled:opacity-50"
>
{working === 'powerCycle' ? 'Cycling...' : 'Power cycle'}
</button>
</div>
<div className="surface-muted grid gap-0.5 pt-0.25">
<p className="text-xs text-slate-300 text-center">Power status</p>
@@ -195,7 +206,7 @@ export default function VipNeatoCard({
</div>
</div>
{!configured || !connected || !canStart || !canSendHome || !canLocate || !canClearErrors ? (
{!configured || !connected || !canStart || !canSendHome || !canLocate || !canClearErrors || !canPowerCycle ? (
<p className="text-xs text-slate-400 text-center">
{!configured
? 'Set homeAssistant.neato.device in server config to enable Neato controls.'
+1
View File
@@ -169,6 +169,7 @@ export function SessionProvider({ children }) {
neatoSendHome: () => emitWithAck('neato:sendHome'),
neatoLocate: () => emitWithAck('neato:locate'),
neatoClearErrors: () => emitWithAck('neato:clearErrors'),
neatoPowerCycle: () => emitWithAck('neato:powerCycle'),
liftUp: () => emitWithAck('lift:up'),
liftDown: () => emitWithAck('lift:down'),
setNickname: (nickname) => emitWithAck('nickname:set', { nickname }),