mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
awaa
This commit is contained in:
@@ -1,3 +1,9 @@
|
|||||||
- discord verification and access requests dont work
|
- discord verification and private access requests dont work. missing important info compared to before, and reactions dont work.
|
||||||
- home assistant idle lights are all messed up. remove from home assistant and make new idle service
|
- replays are sent twice sometimes, look into it.
|
||||||
- snapshots are not working
|
- home assistant idle lights are all messed up. remove from home assistant and make new idle service.
|
||||||
|
- idle service will trigger, after 2 minutes of no drivers:
|
||||||
|
- all room lights (room controls) off
|
||||||
|
- tell all rovers to dock
|
||||||
|
- turn off all rover night vision lights
|
||||||
|
- tell the neato to return to home
|
||||||
|
- the idle service should be easily expandable to add more things in the future
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ require('./src/services/embedHttpService');
|
|||||||
require('./src/services/logStreamService');
|
require('./src/services/logStreamService');
|
||||||
require('./src/services/adminLogService');
|
require('./src/services/adminLogService');
|
||||||
require('./src/services/homeAssistantService');
|
require('./src/services/homeAssistantService');
|
||||||
|
require('./src/services/idleService');
|
||||||
require('./src/services/neatoService');
|
require('./src/services/neatoService');
|
||||||
require('./src/services/audioLevelsService');
|
require('./src/services/audioLevelsService');
|
||||||
require('./src/services/audioForwardService');
|
require('./src/services/audioForwardService');
|
||||||
|
|||||||
@@ -25,7 +25,16 @@ function createDmModerationHandlers(deps) {
|
|||||||
const payload = event?.payload || {};
|
const payload = event?.payload || {};
|
||||||
const requestId = payload.id;
|
const requestId = payload.id;
|
||||||
if (!requestId) return;
|
if (!requestId) return;
|
||||||
const content = [`**Verification Request**`, `Request ID: \`${requestId}\``, `Nickname: ${sanitizeMentions(payload.nickname || 'unknown')}`, '', `React with ${APPROVE} to approve or ${DENY} to deny.`].join('\n');
|
const content = [
|
||||||
|
'**Verification Request**',
|
||||||
|
`Request ID: \`${requestId}\``,
|
||||||
|
`Nickname: ${sanitizeMentions(payload.nickname || 'unknown')}`,
|
||||||
|
`Identity Key: \`${String(payload.cookieUserId || 'unknown')}\``,
|
||||||
|
`IP: \`${String(payload.ip || 'unknown')}\``,
|
||||||
|
`Socket: \`${String(payload.socketId || 'unknown')}\``,
|
||||||
|
'',
|
||||||
|
`React with ${APPROVE} to approve or ${DENY} to deny.`,
|
||||||
|
].join('\n');
|
||||||
await Promise.all(Array.from(lockdownAdminIds).map(async (adminId) => {
|
await Promise.all(Array.from(lockdownAdminIds).map(async (adminId) => {
|
||||||
try {
|
try {
|
||||||
const user = await client.users.fetch(String(adminId));
|
const user = await client.users.fetch(String(adminId));
|
||||||
@@ -44,7 +53,20 @@ function createDmModerationHandlers(deps) {
|
|||||||
const payload = event?.payload || {};
|
const payload = event?.payload || {};
|
||||||
const requestId = payload.id;
|
const requestId = payload.id;
|
||||||
if (!requestId) return;
|
if (!requestId) return;
|
||||||
const content = [`**Private Rover Access Request**`, `Request ID: \`${requestId}\``, '', `React with ${APPROVE} to approve or ${DENY} to deny.`].join('\n');
|
const requester = payload?.requester || {};
|
||||||
|
const content = [
|
||||||
|
'**Private Rover Access Request**',
|
||||||
|
`Request ID: \`${requestId}\``,
|
||||||
|
`Rover: ${sanitizeMentions(payload.roverName || payload.roverId || 'unknown')} (\`${String(payload.roverId || 'unknown')}\`)`,
|
||||||
|
`Nickname: ${sanitizeMentions(requester.nickname || 'unknown')}`,
|
||||||
|
`Role: \`${String(requester.role || 'unknown')}\``,
|
||||||
|
`Verified: \`${requester.isVerified ? 'yes' : 'no'}\``,
|
||||||
|
`Identity Key: \`${String(requester.cookieUserId || 'unknown')}\``,
|
||||||
|
`IP: \`${String(requester.ip || 'unknown')}\``,
|
||||||
|
`Socket: \`${String(requester.socketId || 'unknown')}\``,
|
||||||
|
'',
|
||||||
|
`React with ${APPROVE} to approve or ${DENY} to deny.`,
|
||||||
|
].join('\n');
|
||||||
await Promise.all(Array.from(lockdownAdminIds).map(async (adminId) => {
|
await Promise.all(Array.from(lockdownAdminIds).map(async (adminId) => {
|
||||||
try {
|
try {
|
||||||
const user = await client.users.fetch(String(adminId));
|
const user = await client.users.fetch(String(adminId));
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function createIntegrations(deps) {
|
|||||||
|
|
||||||
const dm = createDmModerationHandlers({ ...deps, sanitizeMentions });
|
const dm = createDmModerationHandlers({ ...deps, sanitizeMentions });
|
||||||
const chat = createChatBridgeHandlers({ ...deps, clearTypingMessage, sendTypingMessage, formatWebhookUsername, getTypingId });
|
const chat = createChatBridgeHandlers({ ...deps, clearTypingMessage, sendTypingMessage, formatWebhookUsername, getTypingId });
|
||||||
const { handleBusEvent, handleReplayRequested } = createBusEventHandler({ ...deps, sendToChannel, schedulePresenceRotation, formatDuration });
|
const { handleBusEvent } = createBusEventHandler({ ...deps, sendToChannel, schedulePresenceRotation, formatDuration });
|
||||||
|
|
||||||
function register() {
|
function register() {
|
||||||
client.on('typingStart', (typing) => {
|
client.on('typingStart', (typing) => {
|
||||||
@@ -32,7 +32,6 @@ function createIntegrations(deps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
subscribe('*', handleBusEvent);
|
subscribe('*', handleBusEvent);
|
||||||
subscribe('replay.requested', handleReplayRequested);
|
|
||||||
subscribe('verification.requested', dm.sendVerificationRequestDms);
|
subscribe('verification.requested', dm.sendVerificationRequestDms);
|
||||||
subscribe('privateRoverAccess.requested', dm.sendPrivateRoverAccessRequestDms);
|
subscribe('privateRoverAccess.requested', dm.sendPrivateRoverAccessRequestDms);
|
||||||
subscribe('chat:message', chat.handleChatBridgeOutbound);
|
subscribe('chat:message', chat.handleChatBridgeOutbound);
|
||||||
|
|||||||
@@ -4,13 +4,11 @@
|
|||||||
const io = require('../../globals/io');
|
const io = require('../../globals/io');
|
||||||
const { getMode, MODES, modeEvents } = require('../modeManager');
|
const { getMode, MODES, modeEvents } = require('../modeManager');
|
||||||
const { isAdmin, isLockdownAdmin } = require('../roleService');
|
const { isAdmin, isLockdownAdmin } = require('../roleService');
|
||||||
const { turnEvents } = require('../turnService');
|
|
||||||
|
|
||||||
function registerHomeAssistantHooks(deps) {
|
function registerHomeAssistantHooks(deps) {
|
||||||
const {
|
const {
|
||||||
logger,
|
logger,
|
||||||
haConfig,
|
haConfig,
|
||||||
evaluateLightAutomation,
|
|
||||||
isLightControlLocked,
|
isLightControlLocked,
|
||||||
setLightsLockedOn,
|
setLightsLockedOn,
|
||||||
toggleEntity,
|
toggleEntity,
|
||||||
@@ -19,26 +17,14 @@ function registerHomeAssistantHooks(deps) {
|
|||||||
setLightWhite,
|
setLightWhite,
|
||||||
} = deps;
|
} = deps;
|
||||||
|
|
||||||
turnEvents.on('activeDriver', () => {
|
|
||||||
evaluateLightAutomation();
|
|
||||||
});
|
|
||||||
|
|
||||||
turnEvents.on('queue', () => {
|
|
||||||
evaluateLightAutomation();
|
|
||||||
});
|
|
||||||
|
|
||||||
modeEvents.on('change', (mode) => {
|
modeEvents.on('change', (mode) => {
|
||||||
if (mode === MODES.ADMIN || mode === MODES.LOCKDOWN) {
|
if (mode === MODES.ADMIN || mode === MODES.LOCKDOWN) {
|
||||||
if (isLightControlLocked()) {
|
if (isLightControlLocked()) {
|
||||||
setLightsLockedOn(false, { source: 'modeGateReset' }).catch((err) => {
|
setLightsLockedOn(false, { source: 'modeGateReset' }).catch((err) => {
|
||||||
logger.warn('Failed to disable lights lock on mode change', err.message);
|
logger.warn('Failed to disable lights lock on mode change', err.message);
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
evaluateLightAutomation();
|
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
evaluateLightAutomation();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
io.on('connection', (socket) => {
|
io.on('connection', (socket) => {
|
||||||
|
|||||||
@@ -36,12 +36,10 @@ callHomeAssistantServiceImpl = transport.callHomeAssistantService;
|
|||||||
runtimeEngine.loadEntityConfig();
|
runtimeEngine.loadEntityConfig();
|
||||||
runtimeEngine.loadTriggerConfig();
|
runtimeEngine.loadTriggerConfig();
|
||||||
transport.connect();
|
transport.connect();
|
||||||
runtimeEngine.evaluateLightAutomation();
|
|
||||||
|
|
||||||
registerHomeAssistantHooks({
|
registerHomeAssistantHooks({
|
||||||
logger,
|
logger,
|
||||||
haConfig,
|
haConfig,
|
||||||
evaluateLightAutomation: runtimeEngine.evaluateLightAutomation,
|
|
||||||
isLightControlLocked: runtimeEngine.isLightControlLocked,
|
isLightControlLocked: runtimeEngine.isLightControlLocked,
|
||||||
setLightsLockedOn: runtimeEngine.setLightsLockedOn,
|
setLightsLockedOn: runtimeEngine.setLightsLockedOn,
|
||||||
toggleEntity: runtimeEngine.toggleEntity,
|
toggleEntity: runtimeEngine.toggleEntity,
|
||||||
@@ -57,11 +55,13 @@ module.exports = {
|
|||||||
getLightPolicyState: runtimeEngine.getLightPolicyState,
|
getLightPolicyState: runtimeEngine.getLightPolicyState,
|
||||||
isLightControlLocked: runtimeEngine.isLightControlLocked,
|
isLightControlLocked: runtimeEngine.isLightControlLocked,
|
||||||
getRawEntitySnapshot: runtimeEngine.getRawEntitySnapshot,
|
getRawEntitySnapshot: runtimeEngine.getRawEntitySnapshot,
|
||||||
|
getControllableEntityIds: runtimeEngine.getControllableEntityIds,
|
||||||
callHomeAssistantService: transport.callHomeAssistantService,
|
callHomeAssistantService: transport.callHomeAssistantService,
|
||||||
toggleEntity: runtimeEngine.toggleEntity,
|
toggleEntity: runtimeEngine.toggleEntity,
|
||||||
setEntityState: runtimeEngine.setEntityState,
|
setEntityState: runtimeEngine.setEntityState,
|
||||||
setLightColor: runtimeEngine.setLightColor,
|
setLightColor: runtimeEngine.setLightColor,
|
||||||
setLightWhite: runtimeEngine.setLightWhite,
|
setLightWhite: runtimeEngine.setLightWhite,
|
||||||
|
setAllControllableEntitiesState: runtimeEngine.setAllControllableEntitiesState,
|
||||||
setLightsLockedOn: runtimeEngine.setLightsLockedOn,
|
setLightsLockedOn: runtimeEngine.setLightsLockedOn,
|
||||||
toggleLightsLockedOn: runtimeEngine.toggleLightsLockedOn,
|
toggleLightsLockedOn: runtimeEngine.toggleLightsLockedOn,
|
||||||
homeAssistantEvents: events,
|
homeAssistantEvents: events,
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
// Home Assistant Runtime Engine
|
// Home Assistant Runtime Engine
|
||||||
// Purpose: Implements entity/trigger processing, light automation policy, and exposed control operations.
|
// Purpose: Implements entity/trigger processing, light automation policy, and exposed control operations.
|
||||||
// Scope: Owns business logic while transport and event wiring are delegated to companion modules.
|
// Scope: Owns business logic while transport and event wiring are delegated to companion modules.
|
||||||
const roverManager = require('../roverManager');
|
|
||||||
const { issueCommand } = require('../commandService');
|
|
||||||
const { publishEvent } = require('../eventBus');
|
|
||||||
const { getMode } = require('../modeManager');
|
const { getMode } = require('../modeManager');
|
||||||
const { getActiveDrivers } = require('../turnService');
|
const { publishEvent } = require('../eventBus');
|
||||||
const {
|
const {
|
||||||
events,
|
events,
|
||||||
entityConfig,
|
entityConfig,
|
||||||
@@ -13,9 +10,7 @@ const {
|
|||||||
triggerConfig,
|
triggerConfig,
|
||||||
triggerRuntime,
|
triggerRuntime,
|
||||||
HA_BUTTON_EVENT_TYPE,
|
HA_BUTTON_EVENT_TYPE,
|
||||||
LIGHT_IDLE_OFF_MS,
|
|
||||||
DEFAULT_WHITE_KELVIN,
|
DEFAULT_WHITE_KELVIN,
|
||||||
NIGHT_VISION_DISABLE_ACTION,
|
|
||||||
runtime,
|
runtime,
|
||||||
} = require('./state');
|
} = require('./state');
|
||||||
const { normalizeConfigEntry, normalizeTriggerEntry, buildState } = require('./entityHelpers');
|
const { normalizeConfigEntry, normalizeTriggerEntry, buildState } = require('./entityHelpers');
|
||||||
@@ -69,56 +64,6 @@ function createRuntimeEngine(deps) {
|
|||||||
return Array.from(entityConfig.values()).map((meta) => String(meta.id));
|
return Array.from(entityConfig.values()).map((meta) => String(meta.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActiveDriverCount() {
|
|
||||||
const active = getActiveDrivers();
|
|
||||||
const turnCount = active && typeof active === 'object' ? Object.keys(active).length : 0;
|
|
||||||
if (turnCount > 0) return turnCount;
|
|
||||||
let liveCount = 0;
|
|
||||||
roverManager.rovers.forEach((record) => {
|
|
||||||
if (record?.drivers?.size > 0) {
|
|
||||||
liveCount += 1;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return liveCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasActiveDrivers() {
|
|
||||||
return getActiveDriverCount() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function turnOffAllRoverNightVision() {
|
|
||||||
const records = Array.from(roverManager.rovers.values());
|
|
||||||
let attempted = 0;
|
|
||||||
let failed = 0;
|
|
||||||
const roverIds = [];
|
|
||||||
records.forEach((record) => {
|
|
||||||
if (!record?.ws) return;
|
|
||||||
roverIds.push(String(record.id));
|
|
||||||
attempted += 1;
|
|
||||||
try {
|
|
||||||
issueCommand(record.id, {
|
|
||||||
type: 'nightVision',
|
|
||||||
nightVision: { action: NIGHT_VISION_DISABLE_ACTION },
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
failed += 1;
|
|
||||||
logger.warn('Failed to auto turn off rover night vision after idle', { roverId: record.id, error: err.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return { attempted, failed, roverIds };
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearLightsIdleOffTimer(getState) {
|
|
||||||
if (runtime.lightsIdleOffTimer) {
|
|
||||||
clearTimeout(runtime.lightsIdleOffTimer);
|
|
||||||
runtime.lightsIdleOffTimer = null;
|
|
||||||
}
|
|
||||||
if (runtime.lightsIdleOffDeadline != null) {
|
|
||||||
runtime.lightsIdleOffDeadline = null;
|
|
||||||
emitUpdate(getState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setEntityState(entityId, desiredState) {
|
async function setEntityState(entityId, desiredState) {
|
||||||
if (!enabled) throw new Error('Home Assistant not configured');
|
if (!enabled) throw new Error('Home Assistant not configured');
|
||||||
const meta = entityConfig.get(entityId);
|
const meta = entityConfig.get(entityId);
|
||||||
@@ -146,35 +91,6 @@ function createRuntimeEngine(deps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleLightsIdleOffTimer(getState, evaluateLightAutomation) {
|
|
||||||
if (!enabled) return;
|
|
||||||
if (getControllableEntityIds().length === 0) return;
|
|
||||||
if (runtime.lightsIdleOffTimer || runtime.lightsLockState != null || hasActiveDrivers()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
runtime.lightsIdleOffDeadline = Date.now() + LIGHT_IDLE_OFF_MS;
|
|
||||||
runtime.lightsIdleOffTimer = setTimeout(async () => {
|
|
||||||
runtime.lightsIdleOffTimer = null;
|
|
||||||
runtime.lightsIdleOffDeadline = null;
|
|
||||||
try {
|
|
||||||
await setAllControllableEntitiesState('off');
|
|
||||||
const nightVisionResult = turnOffAllRoverNightVision();
|
|
||||||
logger.info('Auto-turned off room lights due to no active drivers', {
|
|
||||||
idleMs: LIGHT_IDLE_OFF_MS,
|
|
||||||
nightVisionRovers: nightVisionResult.attempted,
|
|
||||||
nightVisionFailures: nightVisionResult.failed,
|
|
||||||
nightVisionRoverIds: nightVisionResult.roverIds,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
logger.warn('Failed auto light-off after idle', err.message);
|
|
||||||
} finally {
|
|
||||||
emitUpdate(getState);
|
|
||||||
evaluateLightAutomation();
|
|
||||||
}
|
|
||||||
}, LIGHT_IDLE_OFF_MS);
|
|
||||||
emitUpdate(getState);
|
|
||||||
}
|
|
||||||
|
|
||||||
function triggerMatches(trigger, raw, runtimeState) {
|
function triggerMatches(trigger, raw, runtimeState) {
|
||||||
if (!raw) return false;
|
if (!raw) return false;
|
||||||
const nextState = raw?.state ?? null;
|
const nextState = raw?.state ?? null;
|
||||||
@@ -247,9 +163,9 @@ function createRuntimeEngine(deps) {
|
|||||||
locked: runtime.lightsLockState != null,
|
locked: runtime.lightsLockState != null,
|
||||||
lockState: runtime.lightsLockState,
|
lockState: runtime.lightsLockState,
|
||||||
lockedOn: runtime.lightsLockState === 'on',
|
lockedOn: runtime.lightsLockState === 'on',
|
||||||
idleOffMs: LIGHT_IDLE_OFF_MS,
|
idleOffMs: null,
|
||||||
idleOffAt: runtime.lightsIdleOffDeadline,
|
idleOffAt: null,
|
||||||
activeDrivers: getActiveDriverCount(),
|
activeDrivers: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,15 +180,7 @@ function createRuntimeEngine(deps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function evaluateLightAutomation() {
|
function evaluateLightAutomation() {
|
||||||
if (runtime.lightsLockState != null) {
|
// Idle automation moved to idleService; HA service only owns explicit room-control lock behavior.
|
||||||
clearLightsIdleOffTimer(getState);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (hasActiveDrivers()) {
|
|
||||||
clearLightsIdleOffTimer(getState);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
scheduleLightsIdleOffTimer(getState, evaluateLightAutomation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEntitySnapshot(snapshot = {}) {
|
function handleEntitySnapshot(snapshot = {}) {
|
||||||
@@ -341,7 +249,6 @@ function createRuntimeEngine(deps) {
|
|||||||
runtime.lightsLockState = nextLockState;
|
runtime.lightsLockState = nextLockState;
|
||||||
|
|
||||||
if (runtime.lightsLockState != null) {
|
if (runtime.lightsLockState != null) {
|
||||||
clearLightsIdleOffTimer(getState);
|
|
||||||
if ((changed || forceApply) && enabled) {
|
if ((changed || forceApply) && enabled) {
|
||||||
await setAllControllableEntitiesState(runtime.lightsLockState);
|
await setAllControllableEntitiesState(runtime.lightsLockState);
|
||||||
}
|
}
|
||||||
@@ -379,10 +286,12 @@ function createRuntimeEngine(deps) {
|
|||||||
getLightPolicyState,
|
getLightPolicyState,
|
||||||
isLightControlLocked,
|
isLightControlLocked,
|
||||||
getRawEntitySnapshot,
|
getRawEntitySnapshot,
|
||||||
|
getControllableEntityIds,
|
||||||
toggleEntity,
|
toggleEntity,
|
||||||
setEntityState,
|
setEntityState,
|
||||||
setLightColor,
|
setLightColor,
|
||||||
setLightWhite,
|
setLightWhite,
|
||||||
|
setAllControllableEntitiesState,
|
||||||
setLightsLockedOn,
|
setLightsLockedOn,
|
||||||
toggleLightsLockedOn,
|
toggleLightsLockedOn,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ const triggerConfig = [];
|
|||||||
const triggerRuntime = new Map();
|
const triggerRuntime = new Map();
|
||||||
|
|
||||||
const HA_BUTTON_EVENT_TYPE = 'ha.button.action';
|
const HA_BUTTON_EVENT_TYPE = 'ha.button.action';
|
||||||
const LIGHT_IDLE_OFF_MS = 2 * 60 * 1000;
|
|
||||||
const DEFAULT_WHITE_KELVIN = 4000;
|
const DEFAULT_WHITE_KELVIN = 4000;
|
||||||
const NIGHT_VISION_DISABLE_ACTION = 'on';
|
|
||||||
|
|
||||||
const runtime = {
|
const runtime = {
|
||||||
latestEntitySnapshot: {},
|
latestEntitySnapshot: {},
|
||||||
@@ -21,8 +19,6 @@ const runtime = {
|
|||||||
reconnectTimer: null,
|
reconnectTimer: null,
|
||||||
connected: false,
|
connected: false,
|
||||||
lightsLockState: null,
|
lightsLockState: null,
|
||||||
lightsIdleOffTimer: null,
|
|
||||||
lightsIdleOffDeadline: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -32,8 +28,6 @@ module.exports = {
|
|||||||
triggerConfig,
|
triggerConfig,
|
||||||
triggerRuntime,
|
triggerRuntime,
|
||||||
HA_BUTTON_EVENT_TYPE,
|
HA_BUTTON_EVENT_TYPE,
|
||||||
LIGHT_IDLE_OFF_MS,
|
|
||||||
DEFAULT_WHITE_KELVIN,
|
DEFAULT_WHITE_KELVIN,
|
||||||
NIGHT_VISION_DISABLE_ACTION,
|
|
||||||
runtime,
|
runtime,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
// Idle Service Actions
|
||||||
|
// Purpose: Implements the expandable action pipeline executed when the system remains idle.
|
||||||
|
// Scope: Provides isolated action handlers and shared execution helpers for idle automation.
|
||||||
|
const logger = require('../../globals/logger').child('idleService');
|
||||||
|
const roverManager = require('../roverManager');
|
||||||
|
const { issueCommand } = require('../commandService');
|
||||||
|
const homeAssistantService = require('../homeAssistantService');
|
||||||
|
const neatoService = require('../neatoService');
|
||||||
|
const {
|
||||||
|
NIGHT_VISION_DISABLE_ACTION,
|
||||||
|
DOCK_COMMAND_BASE64,
|
||||||
|
} = require('./constants');
|
||||||
|
|
||||||
|
async function turnOffRoomControls() {
|
||||||
|
await homeAssistantService.setAllControllableEntitiesState('off');
|
||||||
|
return { action: 'roomControlsOff' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dockAllRovers() {
|
||||||
|
const attempted = [];
|
||||||
|
const failed = [];
|
||||||
|
roverManager.rovers.forEach((record) => {
|
||||||
|
if (!record?.ws) return;
|
||||||
|
const roverId = String(record.id);
|
||||||
|
try {
|
||||||
|
issueCommand(roverId, { type: 'raw', raw: DOCK_COMMAND_BASE64 });
|
||||||
|
attempted.push(roverId);
|
||||||
|
} catch (err) {
|
||||||
|
failed.push({ roverId, error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { action: 'dockAllRovers', attempted, failed };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disableAllRoverNightVision() {
|
||||||
|
const attempted = [];
|
||||||
|
const failed = [];
|
||||||
|
roverManager.rovers.forEach((record) => {
|
||||||
|
if (!record?.ws) return;
|
||||||
|
const roverId = String(record.id);
|
||||||
|
try {
|
||||||
|
issueCommand(roverId, {
|
||||||
|
type: 'nightVision',
|
||||||
|
nightVision: { action: NIGHT_VISION_DISABLE_ACTION },
|
||||||
|
});
|
||||||
|
attempted.push(roverId);
|
||||||
|
} catch (err) {
|
||||||
|
failed.push({ roverId, error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { action: 'disableRoverNightVision', attempted, failed };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendNeatoHome() {
|
||||||
|
try {
|
||||||
|
await neatoService.sendHome();
|
||||||
|
return { action: 'neatoSendHome', success: true };
|
||||||
|
} catch (err) {
|
||||||
|
return { action: 'neatoSendHome', success: false, error: err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const idleActions = [
|
||||||
|
turnOffRoomControls,
|
||||||
|
dockAllRovers,
|
||||||
|
disableAllRoverNightVision,
|
||||||
|
sendNeatoHome,
|
||||||
|
];
|
||||||
|
|
||||||
|
async function runIdleActions() {
|
||||||
|
const results = [];
|
||||||
|
for (const action of idleActions) {
|
||||||
|
try {
|
||||||
|
const result = await action();
|
||||||
|
results.push({ ok: true, result });
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn('Idle action failed', { action: action.name, error: err.message });
|
||||||
|
results.push({ ok: false, action: action.name, error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
runIdleActions,
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// Idle Service Constants
|
||||||
|
// Purpose: Defines idle timing and command constants used by idle automation workflows.
|
||||||
|
// Scope: Centralizes immutable configuration for trigger windows and rover command payloads.
|
||||||
|
const IDLE_TIMEOUT_MS = 2 * 60 * 1000;
|
||||||
|
const NIGHT_VISION_DISABLE_ACTION = 'on';
|
||||||
|
const DOCK_COMMAND_BASE64 = Buffer.from([143]).toString('base64');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
IDLE_TIMEOUT_MS,
|
||||||
|
NIGHT_VISION_DISABLE_ACTION,
|
||||||
|
DOCK_COMMAND_BASE64,
|
||||||
|
};
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// Idle Service
|
||||||
|
// Purpose: Triggers a modular idle action pipeline after a sustained no-driver period.
|
||||||
|
// Scope: Observes driver activity events and coordinates timer-based idle automation execution.
|
||||||
|
const logger = require('../../globals/logger').child('idleService');
|
||||||
|
const { getActiveDrivers, turnEvents } = require('../turnService');
|
||||||
|
const roverManager = require('../roverManager');
|
||||||
|
const { IDLE_TIMEOUT_MS } = require('./constants');
|
||||||
|
const { runtime } = require('./state');
|
||||||
|
const { runIdleActions } = require('./actions');
|
||||||
|
|
||||||
|
function getActiveDriverCount() {
|
||||||
|
const active = getActiveDrivers();
|
||||||
|
const turnCount = active && typeof active === 'object' ? Object.keys(active).length : 0;
|
||||||
|
if (turnCount > 0) return turnCount;
|
||||||
|
let liveCount = 0;
|
||||||
|
roverManager.rovers.forEach((record) => {
|
||||||
|
if (record?.drivers?.size > 0) liveCount += 1;
|
||||||
|
});
|
||||||
|
return liveCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearIdleTimer() {
|
||||||
|
if (runtime.timer) {
|
||||||
|
clearTimeout(runtime.timer);
|
||||||
|
runtime.timer = null;
|
||||||
|
}
|
||||||
|
runtime.deadlineAt = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleIdleTimer() {
|
||||||
|
if (runtime.timer) return;
|
||||||
|
runtime.deadlineAt = Date.now() + IDLE_TIMEOUT_MS;
|
||||||
|
runtime.timer = setTimeout(async () => {
|
||||||
|
runtime.timer = null;
|
||||||
|
runtime.deadlineAt = null;
|
||||||
|
if (getActiveDriverCount() > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runtime.lastTriggeredAt = Date.now();
|
||||||
|
const results = await runIdleActions();
|
||||||
|
logger.info('Idle automation executed', {
|
||||||
|
idleMs: IDLE_TIMEOUT_MS,
|
||||||
|
resultCount: results.length,
|
||||||
|
failures: results.filter((entry) => !entry.ok).length,
|
||||||
|
});
|
||||||
|
refreshIdleState();
|
||||||
|
}, IDLE_TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshIdleState() {
|
||||||
|
if (getActiveDriverCount() > 0) {
|
||||||
|
clearIdleTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scheduleIdleTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
turnEvents.on('activeDriver', refreshIdleState);
|
||||||
|
turnEvents.on('queue', refreshIdleState);
|
||||||
|
|
||||||
|
refreshIdleState();
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
refreshIdleState,
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// Idle Service State
|
||||||
|
// Purpose: Stores runtime timer and bookkeeping state for idle automation scheduling.
|
||||||
|
// Scope: Encapsulates mutable idle-tracking data shared by idle service modules.
|
||||||
|
const runtime = {
|
||||||
|
timer: null,
|
||||||
|
deadlineAt: null,
|
||||||
|
lastTriggeredAt: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
runtime,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user