mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-18 10:30:47 -04:00
service live configslop
This commit is contained in:
@@ -10,7 +10,7 @@ module.exports = {
|
||||
// ESPHome naming shape while `enabled: false` prevents accidental control.
|
||||
defaultValue: { enabled: false, device: 'neato_vacuum' },
|
||||
schema: strictObject({
|
||||
enabled: boolean({ description: 'Exposes Neato status and commands through the configured Home Assistant ESPHome device after restart.' }),
|
||||
enabled: boolean({ description: 'Immediately exposes Neato status and commands through the configured Home Assistant ESPHome device.' }),
|
||||
device: string({ description: 'ESPHome device name used to derive the Neato entity IDs in Home Assistant; punctuation is normalized to underscores.', examples: ['neato_vacuum'], maxLength: 255 }),
|
||||
}, {
|
||||
title: 'Neato',
|
||||
|
||||
@@ -4,24 +4,16 @@
|
||||
const EventEmitter = require('events');
|
||||
const io = require('../../globals/io');
|
||||
const logger = require('../../globals/logger').child('neatoService');
|
||||
const { loadConfig } = require('../../configuration');
|
||||
const { loadConfig, registerConfigurationHandler } = require('../../configuration');
|
||||
const { isVerified } = require('../verificationService');
|
||||
const { getMode, MODES } = require('../modeManager');
|
||||
const { isAdmin, isLockdownAdmin } = require('../roleService');
|
||||
const { sendAlert } = require('../alertService');
|
||||
const {
|
||||
homeAssistantEvents,
|
||||
getRawEntitySnapshot,
|
||||
callHomeAssistantService,
|
||||
isConnected: isHomeAssistantConnected,
|
||||
enabled: homeAssistantEnabled,
|
||||
} = require('../homeAssistantService');
|
||||
const homeAssistantService = require('../homeAssistantService');
|
||||
const { homeAssistantEvents, getRawEntitySnapshot, callHomeAssistantService } = homeAssistantService;
|
||||
|
||||
const events = new EventEmitter();
|
||||
const config = loadConfig();
|
||||
const haConfig = config.homeAssistant || {};
|
||||
const neatoConfig = haConfig.neato || {};
|
||||
const featureEnabled = Boolean(neatoConfig.enabled);
|
||||
let featureEnabled;
|
||||
|
||||
function normalizeDeviceName(value) {
|
||||
const raw = String(value || '').trim().toLowerCase();
|
||||
@@ -29,7 +21,7 @@ function normalizeDeviceName(value) {
|
||||
return raw.replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, '');
|
||||
}
|
||||
|
||||
const device = normalizeDeviceName(neatoConfig.device);
|
||||
let device;
|
||||
const RESUME_DELAY_MS = 3000;
|
||||
const ALERT_COLOR = '#a855f7';
|
||||
// BrainSlug exposes these exact select values for Gen 3 robots. Keeping the
|
||||
@@ -42,7 +34,11 @@ function entityId(domain, suffix) {
|
||||
return `${domain}.${device}_${suffix}`;
|
||||
}
|
||||
|
||||
const ENTITY_IDS = {
|
||||
let ENTITY_IDS;
|
||||
let ALERT_ENTITIES;
|
||||
|
||||
function buildEntityIds() {
|
||||
return {
|
||||
buttons: {
|
||||
start: entityId('button', 'house_clean'),
|
||||
resume: entityId('button', 'resume_cleaning'),
|
||||
@@ -69,18 +65,26 @@ const ENTITY_IDS = {
|
||||
selects: {
|
||||
navigationMode: entityId('select', 'navigation_mode'),
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// Alert Feed coverage is intentionally limited to the raw robot lifecycle and
|
||||
// issue fields requested for Neato. Battery and charger telemetry poll often and
|
||||
// would create noise without representing a useful robot status transition.
|
||||
const ALERT_ENTITIES = Object.freeze([
|
||||
{ title: 'Neato UI state', entityId: ENTITY_IDS.textSensors.uiState },
|
||||
{ title: 'Neato robot state', entityId: ENTITY_IDS.textSensors.robotState },
|
||||
{ title: 'Neato robot alert', entityId: ENTITY_IDS.textSensors.robotAlert },
|
||||
{ title: 'Neato robot error', entityId: ENTITY_IDS.textSensors.robotError },
|
||||
{ title: 'Neato external power', entityId: ENTITY_IDS.binarySensors.extPowerPresent },
|
||||
]);
|
||||
function applyNeatoConfig(neatoConfig = {}) {
|
||||
featureEnabled = Boolean(neatoConfig.enabled);
|
||||
device = normalizeDeviceName(neatoConfig.device);
|
||||
ENTITY_IDS = buildEntityIds();
|
||||
ALERT_ENTITIES = [
|
||||
{ title: 'Neato UI state', entityId: ENTITY_IDS.textSensors.uiState },
|
||||
{ title: 'Neato robot state', entityId: ENTITY_IDS.textSensors.robotState },
|
||||
{ title: 'Neato robot alert', entityId: ENTITY_IDS.textSensors.robotAlert },
|
||||
{ title: 'Neato robot error', entityId: ENTITY_IDS.textSensors.robotError },
|
||||
{ title: 'Neato external power', entityId: ENTITY_IDS.binarySensors.extPowerPresent },
|
||||
];
|
||||
}
|
||||
|
||||
applyNeatoConfig(loadConfig().homeAssistant?.neato || {});
|
||||
|
||||
// Each entity establishes its own baseline because ESPHome entities can become
|
||||
// available on different snapshots. A Map also distinguishes "not observed yet"
|
||||
@@ -165,7 +169,9 @@ function requiredEntityIds() {
|
||||
|
||||
function buildState() {
|
||||
const configured = Boolean(device);
|
||||
const haConnected = isHomeAssistantConnected();
|
||||
// Home Assistant may have replaced its transport since this service module
|
||||
// loaded, so readiness must be resolved from the live service object.
|
||||
const haConnected = homeAssistantService.isConnected();
|
||||
const requiredIds = requiredEntityIds();
|
||||
const entitiesAvailable = requiredIds.length > 0 && requiredIds.every((id) => isEntityAvailable(id));
|
||||
const connected = Boolean(haConnected && entitiesAvailable);
|
||||
@@ -249,21 +255,14 @@ function emitUpdate() {
|
||||
}
|
||||
}
|
||||
|
||||
if (featureEnabled) {
|
||||
/*
|
||||
Neato telemetry is derived from Home Assistant entities. Disabled installs
|
||||
should keep the exported API inert instead of tracking HA snapshots for a
|
||||
robot vacuum feature that does not exist on that server.
|
||||
*/
|
||||
homeAssistantEvents.on('snapshot', () => {
|
||||
homeAssistantEvents.on('snapshot', () => {
|
||||
if (featureEnabled) {
|
||||
emitUpdate();
|
||||
emitRawStateAlerts();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
homeAssistantEvents.on('status', () => {
|
||||
emitUpdate();
|
||||
});
|
||||
}
|
||||
homeAssistantEvents.on('status', emitUpdate);
|
||||
|
||||
function assertConfiguredAndConnected() {
|
||||
if (!featureEnabled) {
|
||||
@@ -272,10 +271,10 @@ function assertConfiguredAndConnected() {
|
||||
if (!device) {
|
||||
throw new Error('Neato not configured');
|
||||
}
|
||||
if (!homeAssistantEnabled) {
|
||||
if (!homeAssistantService.enabled) {
|
||||
throw new Error('Home Assistant not configured');
|
||||
}
|
||||
if (!isHomeAssistantConnected()) {
|
||||
if (!homeAssistantService.isConnected()) {
|
||||
throw new Error('Home Assistant not connected');
|
||||
}
|
||||
}
|
||||
@@ -349,8 +348,7 @@ function hasVerifiedSockets() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (featureEnabled) {
|
||||
io.on('connection', (socket) => {
|
||||
io.on('connection', (socket) => {
|
||||
function assertFeatureAccess() {
|
||||
const mode = getMode();
|
||||
// Neato shares the same public-activity policy as lift: everyone may use
|
||||
@@ -420,11 +418,18 @@ if (featureEnabled) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
});
|
||||
|
||||
if (!featureEnabled) {
|
||||
logger.info('Neato disabled by config');
|
||||
}
|
||||
|
||||
registerConfigurationHandler('homeAssistant', (haConfig = {}) => {
|
||||
applyNeatoConfig(haConfig.neato || {});
|
||||
alertBaselines.clear();
|
||||
emitUpdate();
|
||||
});
|
||||
|
||||
emitUpdate();
|
||||
|
||||
module.exports = {
|
||||
|
||||
Reference in New Issue
Block a user