mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-11-14 18:58:49 -05:00
59 lines
3.3 KiB
JavaScript
59 lines
3.3 KiB
JavaScript
// User and automated commands share validation; only trusted automation bypasses user permissions.
|
|
const { buildEntity, buildCommand } = require('./entityHelpers');
|
|
|
|
function assertAccess(actor = {}) {
|
|
if (!['user', 'admin', 'lockdown'].includes(actor.role)) throw new Error('Spectators cannot control activities');
|
|
if (actor.mode === 'lockdown' && actor.role !== 'lockdown') throw new Error('Server in lockdown');
|
|
if (actor.mode === 'admin' && !['admin', 'lockdown'].includes(actor.role)) throw new Error('Admin mode: admins only');
|
|
}
|
|
|
|
function createActions({ getConfig, ha, locks }) {
|
|
async function execute(id, value, actor, automated = false) {
|
|
if (!automated) assertAccess(actor);
|
|
const config = getConfig();
|
|
if (!config.enabled) throw new Error('Activity Controls are disabled');
|
|
const item = config.items.find((entry) => entry.id === id);
|
|
if (!item) throw new Error('Unknown activity item');
|
|
if (!automated && locks.isLocked(id) && !['admin', 'lockdown'].includes(actor.role)) throw new Error('This item is locked');
|
|
if (!automated && item.readOnly && !['admin', 'lockdown'].includes(actor.role)) throw new Error('This item is read-only for users');
|
|
if (!ha.enabled || !ha.isConnected()) throw new Error('Home Assistant is offline');
|
|
const entity = buildEntity(item, ha.getRawEntitySnapshot(id));
|
|
if (!entity.available) throw new Error('This item is unavailable');
|
|
const command = buildCommand(entity, value);
|
|
// Let HA process independent commands normally; an outstanding service
|
|
// response must not block later user changes or configured idle cleanup.
|
|
await ha.callHomeAssistantService(entity.domain, command.service, command.data);
|
|
}
|
|
|
|
async function runAutomation(trigger, shouldRun = () => true) {
|
|
const config = getConfig();
|
|
if (!config.enabled) return { action: trigger, skipped: true };
|
|
const results = [];
|
|
// Catch per item so one offline integration cannot prevent the remaining
|
|
// configured actions from running for this trigger.
|
|
for (const item of config.items) {
|
|
// Stop remaining writes if presence or mode changed while HA was responding.
|
|
if (!shouldRun()) break;
|
|
const setting = item.automations?.[trigger];
|
|
if (!setting?.action || setting.action === 'unchanged') continue;
|
|
try {
|
|
const entity = buildEntity(item, ha.getRawEntitySnapshot(item.id));
|
|
if (entity.type === 'readOnly') continue;
|
|
if ((setting.action === 'press') !== (entity.type === 'button')) throw new Error('Automation action does not match the control type');
|
|
// The admin form omits an empty optional string. Treat that as empty
|
|
// text so clearing a message works; other types still reject it through
|
|
// their normal value validation rather than silently receiving zero.
|
|
await execute(item.id, setting.action === 'press' ? 'press' : (setting.value ?? ''), null, true);
|
|
results.push({ id: item.id, ok: true });
|
|
} catch (error) {
|
|
results.push({ id: item.id, ok: false, error: error.message });
|
|
}
|
|
}
|
|
return { action: trigger, results };
|
|
}
|
|
return { act: (id, value, actor) => execute(id, value, actor), runAutomation,
|
|
runIdleActions: () => runAutomation('idle') };
|
|
}
|
|
|
|
module.exports = { createActions, assertAccess };
|