fixes, changes, no more periodic sync. since theres enough activity at this point...

This commit is contained in:
legop3
2026-04-30 13:18:19 -04:00
parent 962d2ae61e
commit b2a2296d98
13 changed files with 133 additions and 84 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-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-w3F8-NIp.js"></script> <script type="module" crossorigin src="/assets/index-DBuBh8sB.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CuroqLuU.css"> <link rel="stylesheet" crossorigin href="/assets/index-CQrwYoqn.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
@@ -34,7 +34,7 @@ function createReplayCommand({ getMode, MODES, tryTriggerReplay, getReplaySource
const roverName = record?.meta?.name || source?.label || roverId; const roverName = record?.meta?.name || source?.label || roverId;
lines.push(`${nickname}${roverName}`); lines.push(`${nickname}${roverName}`);
}); });
if (!lines.length) return ''; if (!lines.length) return 'Drivers: none active for selected rover sources';
return `Drivers: ${lines.join(' | ')}`; return `Drivers: ${lines.join(' | ')}`;
} }
function resolveReplaySources(query) { function resolveReplaySources(query) {
@@ -56,7 +56,7 @@ function createBusEventHandler(deps) {
const roverName = record?.meta?.name || source?.label || roverId; const roverName = record?.meta?.name || source?.label || roverId;
lines.push(`${nickname}${roverName}`); lines.push(`${nickname}${roverName}`);
}); });
if (!lines.length) return ''; if (!lines.length) return 'Drivers: none active for selected rover sources';
return `Drivers: ${lines.join(' | ')}`; return `Drivers: ${lines.join(' | ')}`;
} }
@@ -93,16 +93,27 @@ function createDmModerationHandlers(deps) {
if (emoji === APPROVE) { if (emoji === APPROVE) {
approveRequest(linked.request.id, user.id); approveRequest(linked.request.id, user.id);
await reaction.message.reply({ await reaction.message.reply({
content: `✅ Verification request \`${linked.request.id}\` approved by ${sanitizeMentions(user.username || user.tag || user.id)}.`, content: [
`✅ Verification request \`${linked.request.id}\` approved by ${sanitizeMentions(user.username || user.tag || user.id)}.`,
`Nickname: ${sanitizeMentions(linked.request.nickname || 'unknown')}`,
`Identity Key: \`${String(linked.request.cookieUserId || 'unknown')}\``,
].join('\n'),
allowedMentions: { parse: [] }, allowedMentions: { parse: [] },
}); });
} else { } else {
denyRequest(linked.request.id, user.id); denyRequest(linked.request.id, user.id);
await reaction.message.reply({ await reaction.message.reply({
content: `❌ Verification request \`${linked.request.id}\` denied by ${sanitizeMentions(user.username || user.tag || user.id)}.`, content: [
`❌ Verification request \`${linked.request.id}\` denied by ${sanitizeMentions(user.username || user.tag || user.id)}.`,
`Nickname: ${sanitizeMentions(linked.request.nickname || 'unknown')}`,
`Identity Key: \`${String(linked.request.cookieUserId || 'unknown')}\``,
].join('\n'),
allowedMentions: { parse: [] }, allowedMentions: { parse: [] },
}); });
} }
try {
await reaction.message.reactions.removeAll();
} catch {}
} }
async function handlePrivateAccessReaction(reaction, user) { async function handlePrivateAccessReaction(reaction, user) {
@@ -117,16 +128,27 @@ function createDmModerationHandlers(deps) {
if (emoji === APPROVE) { if (emoji === APPROVE) {
approvePrivateAccessRequest(linked.request.id, user.id); approvePrivateAccessRequest(linked.request.id, user.id);
await reaction.message.reply({ await reaction.message.reply({
content: `✅ Private access request \`${linked.request.id}\` approved by ${sanitizeMentions(user.username || user.tag || user.id)}.`, content: [
`✅ Private access request \`${linked.request.id}\` approved by ${sanitizeMentions(user.username || user.tag || user.id)}.`,
`Rover: ${sanitizeMentions(linked.request.roverName || linked.request.roverId || 'unknown')} (\`${String(linked.request.roverId || 'unknown')}\`)`,
`Requester: ${sanitizeMentions(linked.request.requester?.nickname || 'unknown')}`,
].join('\n'),
allowedMentions: { parse: [] }, allowedMentions: { parse: [] },
}); });
} else { } else {
denyPrivateAccessRequest(linked.request.id, user.id); denyPrivateAccessRequest(linked.request.id, user.id);
await reaction.message.reply({ await reaction.message.reply({
content: `❌ Private access request \`${linked.request.id}\` denied by ${sanitizeMentions(user.username || user.tag || user.id)}.`, content: [
`❌ Private access request \`${linked.request.id}\` denied by ${sanitizeMentions(user.username || user.tag || user.id)}.`,
`Rover: ${sanitizeMentions(linked.request.roverName || linked.request.roverId || 'unknown')} (\`${String(linked.request.roverId || 'unknown')}\`)`,
`Requester: ${sanitizeMentions(linked.request.requester?.nickname || 'unknown')}`,
].join('\n'),
allowedMentions: { parse: [] }, allowedMentions: { parse: [] },
}); });
} }
try {
await reaction.message.reactions.removeAll();
} catch {}
} }
return { return {
@@ -141,8 +141,7 @@ function createRuntimeEngine(deps) {
const nextUpdated = raw?.last_updated ?? null; const nextUpdated = raw?.last_updated ?? null;
const changed = const changed =
runtimeState.lastState !== nextState || runtimeState.lastState !== nextState ||
runtimeState.lastChanged !== nextChanged || runtimeState.lastChanged !== nextChanged;
runtimeState.lastUpdated !== nextUpdated;
if (!changed) { if (!changed) {
return { matched: false, nextState, nextChanged, nextUpdated }; return { matched: false, nextState, nextChanged, nextUpdated };
} }
@@ -13,6 +13,21 @@ const {
} = require('./constants'); } = require('./constants');
async function turnOffRoomControls() { async function turnOffRoomControls() {
const lightPolicy = homeAssistantService.getLightPolicyState?.() || null;
const lockState = lightPolicy?.lockState || null;
if (lockState === 'on') {
logger.info('Idle room-controls off skipped because room controls are locked on', {
lockState,
source: 'idleService:turnOffRoomControls',
});
return {
action: 'roomControlsOff',
skipped: true,
reason: 'lightsLockedOn',
lockState,
};
}
const before = homeAssistantService.getState?.().entities || []; const before = homeAssistantService.getState?.().entities || [];
const summary = await homeAssistantService.setAllControllableEntitiesState('off', { const summary = await homeAssistantService.setAllControllableEntitiesState('off', {
source: 'idleService:turnOffRoomControls', source: 'idleService:turnOffRoomControls',
@@ -49,6 +49,7 @@ const ENTITY_IDS = {
}, },
textSensors: { textSensors: {
// ESPHome text_sensor entities surface in Home Assistant under the sensor domain. // ESPHome text_sensor entities surface in Home Assistant under the sensor domain.
robotState: entityId('sensor', 'robot_state'),
uiState: entityId('sensor', 'ui_state'), uiState: entityId('sensor', 'ui_state'),
robotError: entityId('sensor', 'robot_error'), robotError: entityId('sensor', 'robot_error'),
robotAlert: entityId('sensor', 'robot_alert'), robotAlert: entityId('sensor', 'robot_alert'),
@@ -96,6 +97,7 @@ function requiredEntityIds() {
ENTITY_IDS.sensors.batteryVoltage, ENTITY_IDS.sensors.batteryVoltage,
ENTITY_IDS.binarySensors.chargingActive, ENTITY_IDS.binarySensors.chargingActive,
ENTITY_IDS.binarySensors.extPowerPresent, ENTITY_IDS.binarySensors.extPowerPresent,
ENTITY_IDS.textSensors.robotState,
ENTITY_IDS.textSensors.uiState, ENTITY_IDS.textSensors.uiState,
ENTITY_IDS.textSensors.robotError, ENTITY_IDS.textSensors.robotError,
ENTITY_IDS.textSensors.robotAlert, ENTITY_IDS.textSensors.robotAlert,
@@ -133,6 +135,7 @@ function buildState() {
const batteryPercent = const batteryPercent =
batteryPercentValue == null ? null : Math.max(0, Math.min(100, Math.round(batteryPercentValue))); batteryPercentValue == null ? null : Math.max(0, Math.min(100, Math.round(batteryPercentValue)));
const batteryVoltage = parseNumber(readState(ENTITY_IDS.sensors.batteryVoltage)); const batteryVoltage = parseNumber(readState(ENTITY_IDS.sensors.batteryVoltage));
const robotState = readState(ENTITY_IDS.textSensors.robotState);
const uiState = readState(ENTITY_IDS.textSensors.uiState); const uiState = readState(ENTITY_IDS.textSensors.uiState);
const robotError = readState(ENTITY_IDS.textSensors.robotError); const robotError = readState(ENTITY_IDS.textSensors.robotError);
const robotAlert = readState(ENTITY_IDS.textSensors.robotAlert); const robotAlert = readState(ENTITY_IDS.textSensors.robotAlert);
@@ -151,6 +154,7 @@ function buildState() {
batteryVoltage, batteryVoltage,
chargingActive, chargingActive,
extPowerPresent, extPowerPresent,
robotState,
uiState, uiState,
robotError, robotError,
robotAlert, robotAlert,
+4 -4
View File
@@ -331,10 +331,10 @@ audioLevelsEvents.on('change', () => {
}); });
// sync all sockets 20 seconds // sync all sockets 20 seconds
setInterval(() => { // setInterval(() => {
logger.info('Periodic session sync for all clients'); // logger.info('Periodic session sync for all clients');
syncAll(); // syncAll();
}, PERIODIC_SYNC_MS); // }, PERIODIC_SYNC_MS);
module.exports = { module.exports = {
buildSession, buildSession,
+4 -4
View File
@@ -13,7 +13,7 @@ function badgeClass(tone) {
function positionLabel(value) { function positionLabel(value) {
if (value === 'up') return 'Up'; if (value === 'up') return 'Up';
if (value === 'down') return 'Down'; if (value === 'down') return 'Down';
if (value === 'stopped') return 'Stopped'; if (value === 'stopped') return 'Transitioning...';
if (value === 'conflict') return 'Conflict'; if (value === 'conflict') return 'Conflict';
return '--'; return '--';
} }
@@ -78,7 +78,7 @@ export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
Controls are disabled while the lift is moving, otherwise it's tiny brain would get confused. Controls are disabled while the lift is moving, otherwise it's tiny brain would get confused.
</p> </p>
{!busy && cooldownActive ? ( {!busy && cooldownActive ? (
<p className="text-xs text-slate-400">Try again in {cooldownSeconds}s.</p> <p className="text-xs text-slate-400">About {cooldownSeconds}s remaining.</p>
) : null} ) : null}
</div> </div>
</div> </div>
@@ -106,7 +106,7 @@ export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
type="button" type="button"
disabled={!canRun} disabled={!canRun}
onClick={() => run('down', onDown)} onClick={() => run('down', onDown)}
className={`rounded-md px-1 py-0.75 text-base font-semibold transition disabled:opacity-50 ${position === 'down' || activeTarget === 'down' ? 'bg-sky-600 text-white' : 'bg-slate-700 text-slate-100 hover:bg-slate-600'}`} className={`button-dark w-full text-sm disabled:opacity-50 ${position === 'down' || activeTarget === 'down' ? 'bg-emerald-500 text-white hover:bg-emerald-500' : ''}`}
> >
{working === 'down' || activeTarget === 'down' ? 'Lowering...' : 'Down'} {working === 'down' || activeTarget === 'down' ? 'Lowering...' : 'Down'}
</button> </button>
@@ -114,7 +114,7 @@ export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
type="button" type="button"
disabled={!canRun} disabled={!canRun}
onClick={() => run('up', onUp)} onClick={() => run('up', onUp)}
className={`rounded-md px-1 py-0.75 text-base font-semibold transition disabled:opacity-50 ${position === 'up' || activeTarget === 'up' ? 'bg-emerald-600 text-white' : 'bg-slate-700 text-slate-100 hover:bg-slate-600'}`} className={`button-dark w-full text-sm disabled:opacity-50 ${position === 'up' || activeTarget === 'up' ? 'bg-emerald-500 text-white hover:bg-emerald-500' : ''}`}
> >
{working === 'up' || activeTarget === 'up' ? 'Raising...' : 'Up'} {working === 'up' || activeTarget === 'up' ? 'Raising...' : 'Up'}
</button> </button>
+66 -57
View File
@@ -24,14 +24,13 @@ function metricToneClass(tone) {
if (tone === 'good') return 'bg-emerald-600 text-white'; if (tone === 'good') return 'bg-emerald-600 text-white';
if (tone === 'warn') return 'bg-amber-500 text-slate-900'; if (tone === 'warn') return 'bg-amber-500 text-slate-900';
if (tone === 'danger') return 'bg-rose-600 text-white'; if (tone === 'danger') return 'bg-rose-600 text-white';
if (tone === 'info') return 'bg-sky-600 text-white';
return 'bg-slate-700 text-slate-100'; return 'bg-slate-700 text-slate-100';
} }
function StatusTile({ label, value, tone = 'muted', valueClass = '' }) { function StatusTile({ label, value, tone = 'muted', valueClass = '', hideLabel = false }) {
return ( return (
<div className={`rounded-md px-1 py-0.75 text-center ${metricToneClass(tone)}`}> <div className={`rounded-md px-1 py-0.75 text-center ${metricToneClass(tone)}`}>
<div className="text-[0.72rem] opacity-90">{label}</div> {!hideLabel ? <div className="text-[0.72rem] opacity-90">{label}</div> : null}
<div className={`text-sm font-semibold ${valueClass}`} title={String(value || '')}> <div className={`text-sm font-semibold ${valueClass}`} title={String(value || '')}>
{value} {value}
</div> </div>
@@ -56,6 +55,7 @@ export default function VipNeatoCard({
const charging = Boolean(neato?.telemetry?.chargingActive); const charging = Boolean(neato?.telemetry?.chargingActive);
const uiStateLabel = humanizeUiState(neato?.telemetry?.uiState); const uiStateLabel = humanizeUiState(neato?.telemetry?.uiState);
const robotStateRaw = normalizeState(neato?.telemetry?.robotState) || '--';
const battery = neato?.telemetry?.batteryPercent; const battery = neato?.telemetry?.batteryPercent;
const batteryLabel = Number.isFinite(battery) ? `${battery}%` : '--'; const batteryLabel = Number.isFinite(battery) ? `${battery}%` : '--';
const voltage = neato?.telemetry?.batteryVoltage; const voltage = neato?.telemetry?.batteryVoltage;
@@ -112,30 +112,26 @@ export default function VipNeatoCard({
</span> </span>
</div> </div>
<section className="surface-muted px-0.5 py-0.5"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-0.5">
<div className="grid grid-cols-1 md:grid-cols-4 gap-0.5"> <div className="surface-muted grid gap-0.5">
<div className="rounded-md bg-slate-800 px-1 py-0.75 text-center"> <p className="text-xs text-slate-300 text-center">Controls</p>
<div className="text-xs text-slate-300">State</div> <button
<div className="text-base font-semibold text-slate-100">{primaryState}</div> type="button"
</div> disabled={!canRunStart || Boolean(working)}
<button onClick={() => runAction('start', onStart)}
type="button" className="rounded-md border border-sky-300 bg-emerald-600 px-1 py-1 text-base font-semibold text-white transition hover:border-sky-500 hover:bg-emerald-500 disabled:opacity-50"
disabled={!canRunStart || Boolean(working)} >
onClick={() => runAction('start', onStart)} {working === 'start' ? 'Starting...' : 'Start cleaning'}
className="rounded-md border border-sky-300 bg-emerald-600 px-1 py-1 text-base font-semibold text-white transition hover:border-sky-500 hover:bg-emerald-500 disabled:opacity-50" </button>
> <button
{working === 'start' ? 'Starting...' : 'Start cleaning'} type="button"
</button> disabled={!canRunSendHome || Boolean(working)}
<button onClick={() => runAction('sendHome', onSendHome)}
type="button" className="rounded-md border border-sky-300 bg-sky-600 px-1 py-1 text-base font-semibold text-white transition hover:border-sky-500 hover:bg-sky-500 disabled:opacity-50"
disabled={!canRunSendHome || Boolean(working)} >
onClick={() => runAction('sendHome', onSendHome)} {working === 'sendHome' ? 'Sending...' : 'Send to dock'}
className="rounded-md border border-sky-300 bg-sky-600 px-1 py-1 text-base font-semibold text-white transition hover:border-sky-500 hover:bg-sky-500 disabled:opacity-50" </button>
> <div className="grid grid-cols-2 gap-0.5">
{working === 'sendHome' ? 'Sending...' : 'Send to dock'}
</button>
<div className="grid grid-cols-2 md:grid-cols-1 gap-0.5">
<div className="rounded-md bg-slate-800 px-1 py-0.75 text-center">
<button <button
type="button" type="button"
disabled={!canRunLocate || Boolean(working)} disabled={!canRunLocate || Boolean(working)}
@@ -144,8 +140,6 @@ export default function VipNeatoCard({
> >
{working === 'locate' ? 'Playing...' : 'Play sound'} {working === 'locate' ? 'Playing...' : 'Play sound'}
</button> </button>
</div>
<div className="rounded-md bg-slate-800 px-1 py-0.75 text-center">
<button <button
type="button" type="button"
disabled={!canRunClearErrors || Boolean(working)} disabled={!canRunClearErrors || Boolean(working)}
@@ -155,36 +149,51 @@ export default function VipNeatoCard({
{working === 'clearErrors' ? 'Clearing...' : 'Clear errors'} {working === 'clearErrors' ? 'Clearing...' : 'Clear errors'}
</button> </button>
</div> </div>
<div className="surface-muted grid gap-0.5 pt-0.25">
<p className="text-xs text-slate-300 text-center">Power status</p>
<div className="grid grid-cols-2 gap-0.5">
<StatusTile label="Battery" value={batteryLabel} tone={batteryTone} />
<StatusTile label="Voltage" value={voltageLabel} tone="muted" />
<StatusTile
label="Docked"
value={docked ? 'Docked' : 'Not docked'}
tone={docked ? 'good' : 'muted'}
hideLabel
/>
<StatusTile
label="Charging"
value={charging ? 'Charging' : 'Not charging'}
tone={charging ? 'good' : 'muted'}
hideLabel
/>
</div>
</div>
</div>
<div className="surface-muted grid gap-0.5">
<p className="text-xs text-slate-300 text-center">Robot Status</p>
<div className="rounded-md bg-slate-800 px-1 py-0.5">
<div className="text-[0.72rem] text-slate-300">Robot state (raw)</div>
<div className="font-mono text-sm text-slate-100 break-all">{robotStateRaw}</div>
</div>
<div className="rounded-md bg-slate-800 px-1 py-0.5">
<div className="text-[0.72rem] text-slate-300">Basic state</div>
<div className="font-mono text-sm text-slate-100 break-all">{primaryState}</div>
</div>
<div className="rounded-md bg-slate-800 px-1 py-0.5">
<div className="text-[0.72rem] text-slate-300">UI state</div>
<div className="font-mono text-sm text-slate-100 break-all">{uiStateLabel}</div>
</div>
<div className="rounded-md bg-slate-800 px-1 py-0.5">
<div className="text-[0.72rem] text-slate-300">Robot error</div>
<div className="font-mono text-sm text-slate-100 break-all">{robotError}</div>
</div>
<div className="rounded-md bg-slate-800 px-1 py-0.5">
<div className="text-[0.72rem] text-slate-300">Robot alert</div>
<div className="font-mono text-sm text-slate-100 break-all">{robotAlert}</div>
</div>
</div> </div>
</div> </div>
</section>
<section className="surface-muted px-0.5 py-0.5">
<div className="grid gap-0.5 grid-cols-2 sm:grid-cols-3 lg:grid-cols-7">
<StatusTile label="Battery" value={batteryLabel} tone={batteryTone} />
<StatusTile label="Voltage" value={voltageLabel} tone={voltage == null ? 'muted' : 'info'} />
<StatusTile label="Docked" value={docked ? 'Yes' : 'No'} tone={docked ? 'info' : 'muted'} />
<StatusTile label="Charging" value={charging ? 'Yes' : 'No'} tone={charging ? 'info' : 'muted'} />
<StatusTile
label="UI state"
value={uiStateLabel}
tone="muted"
valueClass="truncate"
/>
<StatusTile
label="Robot error"
value={robotError}
tone={hasError ? 'danger' : 'muted'}
valueClass="truncate"
/>
<StatusTile
label="Robot alert"
value={robotAlert}
tone={hasAlert ? 'warn' : 'muted'}
valueClass="truncate"
/>
</div>
</section>
{!configured || !connected || !canStart || !canSendHome || !canLocate || !canClearErrors ? ( {!configured || !connected || !canStart || !canSendHome || !canLocate || !canClearErrors ? (
<p className="text-xs text-slate-400 text-center"> <p className="text-xs text-slate-400 text-center">