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-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-w3F8-NIp.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CuroqLuU.css">
<script type="module" crossorigin src="/assets/index-DBuBh8sB.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CQrwYoqn.css">
</head>
<body>
<div id="root"></div>
@@ -34,7 +34,7 @@ function createReplayCommand({ getMode, MODES, tryTriggerReplay, getReplaySource
const roverName = record?.meta?.name || source?.label || roverId;
lines.push(`${nickname}${roverName}`);
});
if (!lines.length) return '';
if (!lines.length) return 'Drivers: none active for selected rover sources';
return `Drivers: ${lines.join(' | ')}`;
}
function resolveReplaySources(query) {
@@ -56,7 +56,7 @@ function createBusEventHandler(deps) {
const roverName = record?.meta?.name || source?.label || roverId;
lines.push(`${nickname}${roverName}`);
});
if (!lines.length) return '';
if (!lines.length) return 'Drivers: none active for selected rover sources';
return `Drivers: ${lines.join(' | ')}`;
}
@@ -93,16 +93,27 @@ function createDmModerationHandlers(deps) {
if (emoji === APPROVE) {
approveRequest(linked.request.id, user.id);
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: [] },
});
} else {
denyRequest(linked.request.id, user.id);
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: [] },
});
}
try {
await reaction.message.reactions.removeAll();
} catch {}
}
async function handlePrivateAccessReaction(reaction, user) {
@@ -117,16 +128,27 @@ function createDmModerationHandlers(deps) {
if (emoji === APPROVE) {
approvePrivateAccessRequest(linked.request.id, user.id);
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: [] },
});
} else {
denyPrivateAccessRequest(linked.request.id, user.id);
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: [] },
});
}
try {
await reaction.message.reactions.removeAll();
} catch {}
}
return {
@@ -141,8 +141,7 @@ function createRuntimeEngine(deps) {
const nextUpdated = raw?.last_updated ?? null;
const changed =
runtimeState.lastState !== nextState ||
runtimeState.lastChanged !== nextChanged ||
runtimeState.lastUpdated !== nextUpdated;
runtimeState.lastChanged !== nextChanged;
if (!changed) {
return { matched: false, nextState, nextChanged, nextUpdated };
}
@@ -13,6 +13,21 @@ const {
} = require('./constants');
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 summary = await homeAssistantService.setAllControllableEntitiesState('off', {
source: 'idleService:turnOffRoomControls',
@@ -49,6 +49,7 @@ const ENTITY_IDS = {
},
textSensors: {
// ESPHome text_sensor entities surface in Home Assistant under the sensor domain.
robotState: entityId('sensor', 'robot_state'),
uiState: entityId('sensor', 'ui_state'),
robotError: entityId('sensor', 'robot_error'),
robotAlert: entityId('sensor', 'robot_alert'),
@@ -96,6 +97,7 @@ function requiredEntityIds() {
ENTITY_IDS.sensors.batteryVoltage,
ENTITY_IDS.binarySensors.chargingActive,
ENTITY_IDS.binarySensors.extPowerPresent,
ENTITY_IDS.textSensors.robotState,
ENTITY_IDS.textSensors.uiState,
ENTITY_IDS.textSensors.robotError,
ENTITY_IDS.textSensors.robotAlert,
@@ -133,6 +135,7 @@ function buildState() {
const batteryPercent =
batteryPercentValue == null ? null : Math.max(0, Math.min(100, Math.round(batteryPercentValue)));
const batteryVoltage = parseNumber(readState(ENTITY_IDS.sensors.batteryVoltage));
const robotState = readState(ENTITY_IDS.textSensors.robotState);
const uiState = readState(ENTITY_IDS.textSensors.uiState);
const robotError = readState(ENTITY_IDS.textSensors.robotError);
const robotAlert = readState(ENTITY_IDS.textSensors.robotAlert);
@@ -151,6 +154,7 @@ function buildState() {
batteryVoltage,
chargingActive,
extPowerPresent,
robotState,
uiState,
robotError,
robotAlert,
+4 -4
View File
@@ -331,10 +331,10 @@ audioLevelsEvents.on('change', () => {
});
// sync all sockets 20 seconds
setInterval(() => {
logger.info('Periodic session sync for all clients');
syncAll();
}, PERIODIC_SYNC_MS);
// setInterval(() => {
// logger.info('Periodic session sync for all clients');
// syncAll();
// }, PERIODIC_SYNC_MS);
module.exports = {
buildSession,
+4 -4
View File
@@ -13,7 +13,7 @@ function badgeClass(tone) {
function positionLabel(value) {
if (value === 'up') return 'Up';
if (value === 'down') return 'Down';
if (value === 'stopped') return 'Stopped';
if (value === 'stopped') return 'Transitioning...';
if (value === 'conflict') return 'Conflict';
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.
</p>
{!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}
</div>
</div>
@@ -106,7 +106,7 @@ export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
type="button"
disabled={!canRun}
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'}
</button>
@@ -114,7 +114,7 @@ export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
type="button"
disabled={!canRun}
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'}
</button>
+66 -57
View File
@@ -24,14 +24,13 @@ function metricToneClass(tone) {
if (tone === 'good') return 'bg-emerald-600 text-white';
if (tone === 'warn') return 'bg-amber-500 text-slate-900';
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';
}
function StatusTile({ label, value, tone = 'muted', valueClass = '' }) {
function StatusTile({ label, value, tone = 'muted', valueClass = '', hideLabel = false }) {
return (
<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 || '')}>
{value}
</div>
@@ -56,6 +55,7 @@ export default function VipNeatoCard({
const charging = Boolean(neato?.telemetry?.chargingActive);
const uiStateLabel = humanizeUiState(neato?.telemetry?.uiState);
const robotStateRaw = normalizeState(neato?.telemetry?.robotState) || '--';
const battery = neato?.telemetry?.batteryPercent;
const batteryLabel = Number.isFinite(battery) ? `${battery}%` : '--';
const voltage = neato?.telemetry?.batteryVoltage;
@@ -112,30 +112,26 @@ export default function VipNeatoCard({
</span>
</div>
<section className="surface-muted px-0.5 py-0.5">
<div className="grid grid-cols-1 md:grid-cols-4 gap-0.5">
<div className="rounded-md bg-slate-800 px-1 py-0.75 text-center">
<div className="text-xs text-slate-300">State</div>
<div className="text-base font-semibold text-slate-100">{primaryState}</div>
</div>
<button
type="button"
disabled={!canRunStart || Boolean(working)}
onClick={() => runAction('start', onStart)}
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"
>
{working === 'start' ? 'Starting...' : 'Start cleaning'}
</button>
<button
type="button"
disabled={!canRunSendHome || Boolean(working)}
onClick={() => runAction('sendHome', onSendHome)}
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"
>
{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">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-0.5">
<div className="surface-muted grid gap-0.5">
<p className="text-xs text-slate-300 text-center">Controls</p>
<button
type="button"
disabled={!canRunStart || Boolean(working)}
onClick={() => runAction('start', onStart)}
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"
>
{working === 'start' ? 'Starting...' : 'Start cleaning'}
</button>
<button
type="button"
disabled={!canRunSendHome || Boolean(working)}
onClick={() => runAction('sendHome', onSendHome)}
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"
>
{working === 'sendHome' ? 'Sending...' : 'Send to dock'}
</button>
<div className="grid grid-cols-2 gap-0.5">
<button
type="button"
disabled={!canRunLocate || Boolean(working)}
@@ -144,8 +140,6 @@ export default function VipNeatoCard({
>
{working === 'locate' ? 'Playing...' : 'Play sound'}
</button>
</div>
<div className="rounded-md bg-slate-800 px-1 py-0.75 text-center">
<button
type="button"
disabled={!canRunClearErrors || Boolean(working)}
@@ -155,36 +149,51 @@ export default function VipNeatoCard({
{working === 'clearErrors' ? 'Clearing...' : 'Clear errors'}
</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>
<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>
</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 ? (
<p className="text-xs text-slate-400 text-center">