mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
fixes, changes, no more periodic sync. since theres enough activity at this point...
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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,12 +112,9 @@ 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>
|
||||
<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)}
|
||||
@@ -134,8 +131,7 @@ export default function VipNeatoCard({
|
||||
>
|
||||
{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-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>
|
||||
</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">
|
||||
<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={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="Voltage" value={voltageLabel} tone="muted" />
|
||||
<StatusTile
|
||||
label="UI state"
|
||||
value={uiStateLabel}
|
||||
tone="muted"
|
||||
valueClass="truncate"
|
||||
label="Docked"
|
||||
value={docked ? 'Docked' : 'Not docked'}
|
||||
tone={docked ? 'good' : 'muted'}
|
||||
hideLabel
|
||||
/>
|
||||
<StatusTile
|
||||
label="Robot error"
|
||||
value={robotError}
|
||||
tone={hasError ? 'danger' : 'muted'}
|
||||
valueClass="truncate"
|
||||
/>
|
||||
<StatusTile
|
||||
label="Robot alert"
|
||||
value={robotAlert}
|
||||
tone={hasAlert ? 'warn' : 'muted'}
|
||||
valueClass="truncate"
|
||||
label="Charging"
|
||||
value={charging ? 'Charging' : 'Not charging'}
|
||||
tone={charging ? 'good' : 'muted'}
|
||||
hideLabel
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</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>
|
||||
|
||||
{!configured || !connected || !canStart || !canSendHome || !canLocate || !canClearErrors ? (
|
||||
<p className="text-xs text-slate-400 text-center">
|
||||
|
||||
Reference in New Issue
Block a user