add more better options for HA activities and automatically changing them (for google home switch lol)

This commit is contained in:
legop3
2026-10-01 02:11:07 -04:00
parent 607d327e9c
commit 1ba4ebe5ff
7 changed files with 87 additions and 32 deletions
@@ -1,5 +1,4 @@
// User and idle commands share one dispatch contract, while only the trusted
// idle entry point can bypass per-item user locks.
// User and automated commands share validation; only trusted automation bypasses user permissions.
const { buildEntity, buildCommand } = require('./entityHelpers');
function assertAccess(actor = {}) {
@@ -9,13 +8,14 @@ function assertAccess(actor = {}) {
}
function createActions({ getConfig, ha, locks }) {
async function execute(id, value, actor, idle = false) {
if (!idle) assertAccess(actor);
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 (!idle && locks.isLocked(id) && !['admin', 'lockdown'].includes(actor.role)) throw new Error('This item is locked');
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');
@@ -25,30 +25,34 @@ function createActions({ getConfig, ha, locks }) {
await ha.callHomeAssistantService(entity.domain, command.service, command.data);
}
async function runIdleActions() {
async function runAutomation(trigger, shouldRun = () => true) {
const config = getConfig();
if (!config.enabled) return { action: 'homeAssistantActivitiesIdle', skipped: true };
if (!config.enabled) return { action: trigger, skipped: true };
const results = [];
// Catch per item so one offline integration cannot prevent the remaining
// configured cleanup actions from running during this idle window.
// configured actions from running for this trigger.
for (const item of config.items) {
if (!item.idleAction || item.idleAction === 'unchanged' || item.readOnly) continue;
// 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 ((item.idleAction === 'press') !== (entity.type === 'button')) throw new Error('Idle action does not match the control type');
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, item.idleAction === 'press' ? 'press' : (item.idleValue ?? ''), null, true);
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: 'homeAssistantActivitiesIdle', results };
return { action: trigger, results };
}
return { act: (id, value, actor) => execute(id, value, actor), runIdleActions };
return { act: (id, value, actor) => execute(id, value, actor), runAutomation,
runIdleActions: () => runAutomation('idle') };
}
module.exports = { createActions, assertAccess };