rename old room controls to lights and rename new thing to activity controls
Container image / image (push) Failing after 1m2s

This commit is contained in:
legop3
2026-09-16 14:45:09 -04:00
parent 51b42ea778
commit 487666eed8
21 changed files with 48 additions and 47 deletions
@@ -44,7 +44,7 @@ module.exports = {
goal: 400,
async run(ctx) {
startStrobe(ctx, { endsAt: Date.now() + STROBE_MS, on: false });
ctx.sendAlert({ color: '#ffc107', title: 'Light Strobe', message: 'All room controls strobing for 60 seconds.' });
ctx.sendAlert({ color: '#ffc107', title: 'Light Strobe', message: 'All room lights strobing for 60 seconds.' });
},
async recover(ctx, effect) {
if (!effect || Number(effect.endsAt || 0) <= Date.now()) {
@@ -52,7 +52,7 @@ async function setEnabled(nextValue, options = {}) {
.map(({ result, entityId }) => ({ entityId, error: result.reason?.message || 'unknown error' }));
if (failures.length) {
logger.warn('Some room controls failed to enter green mode', { failures });
logger.warn('Some room lights failed to enter green mode', { failures });
}
} else if (!next && homeAssistantService.enabled) {
// Disabling the visual mode simply releases the lock it created. Bulb
@@ -67,7 +67,7 @@ async function setEnabled(nextValue, options = {}) {
skipping the physical-room operations still allows the session theme,
CardFrame styling, alerts, commands, and timed reward to work normally.
The integration's generic lock state is also left untouched because there
are no server-managed room controls to lock.
are no server-managed room lights to lock.
*/
enabled = next;
@@ -12,7 +12,7 @@ function createActions({ getConfig, ha, locks }) {
async function execute(id, value, actor, idle = false) {
if (!idle) assertAccess(actor);
const config = getConfig();
if (!config.enabled) throw new Error('Home Assistant activities are disabled');
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');
@@ -129,7 +129,8 @@ test('locks are process-local and names resolve without guessing', () => {
assert.equal(resolveItem(items.slice(0, 1), 'FAN SPEED').id, 'number.fan');
assert.equal(resolveItem(items, 'NUMBER.FAN').id, 'number.fan');
assert.throws(() => resolveItem(items, 'Fan speed'), /number.fan, number.other/);
assert.throws(() => resolveItem(items, 'Fan'), /No activity/);
// Keep the lookup error aligned with the panel name users see when choosing an item.
assert.throws(() => resolveItem(items, 'Fan'), /No activity control/);
});
test('command preserves multiword names and exposes lock status', async () => {
@@ -1,4 +1,4 @@
// Home Assistant activities have their own catalog so room-light bulk actions
// Activity Controls have their own catalog so room-light bulk actions
// never acquire unrelated devices simply because they share a connection.
const { strictObject, string, boolean } = require('../../configuration/schemaHelpers');
@@ -7,13 +7,13 @@ module.exports = {
feature: true,
defaultValue: { enabled: false, items: [] },
schema: strictObject({
enabled: boolean({ description: 'Shows the separate Activities card using the enabled Home Assistant connection.' }),
enabled: boolean({ description: 'Shows the Activity Controls card in Activities using the enabled Home Assistant connection.' }),
items: {
type: 'array',
title: 'Activity items',
title: 'Activity control items',
description: 'Ordered public entities. Control types and limits come from Home Assistant; unsupported domains are read-only.',
items: strictObject({
id: string({ title: 'Entity id', description: 'Exact Home Assistant entity ID to expose in Activities.', pattern: '^[a-z0-9_]+\\.[a-z0-9_]+$', maxLength: 255, examples: ['input_number.fan_speed'] }),
id: string({ title: 'Entity id', description: 'Exact Home Assistant entity ID to expose in Activity Controls.', pattern: '^[a-z0-9_]+\\.[a-z0-9_]+$', maxLength: 255, examples: ['input_number.fan_speed'] }),
name: string({ title: 'Display name', description: 'Optional override; otherwise uses the Home Assistant friendly name.', maxLength: 120, examples: ['Fan speed'] }),
icon: string({ description: 'Font Awesome name, as used by social links. Blank or invalid names use a fallback.', maxLength: 80, examples: ['FaFan'] }),
// Match social-link colors so admins can customize tiles through the existing config form.
@@ -21,7 +21,7 @@ module.exports = {
readOnly: boolean({ title: 'Read only', default: false, description: 'Display the value without allowing user or idle commands.' }),
idleAction: string({ title: 'When idle', enum: ['unchanged', 'set', 'press'], default: 'unchanged', description: 'Leave unchanged, set the idle value, or press a button once. Runs independently of user locks.' }),
idleValue: string({ title: 'Idle value', description: 'For set: on/off, a number, exact selection, or text (empty text clears it). Checked against live entity limits when idle runs.', examples: ['0'], maxLength: 255 }),
}, { description: 'One independently controlled or read-only activity item.', required: ['id'] }),
}, { description: 'One independently controlled or read-only activity control item.', required: ['id'] }),
},
}, { title: 'Home Assistant activities', description: 'Generic activity controls and idle behavior, separate from room lighting.', required: ['enabled', 'items'] }),
}, { title: 'Activity Controls', description: 'Generic activity controls and idle behavior, separate from room lighting.', required: ['enabled', 'items'] }),
};
@@ -19,7 +19,7 @@ function resolveItem(items, query) {
if (idMatch) return idMatch;
const matches = items.filter((item) => item.name.toLowerCase() === normalized);
if (matches.length > 1) throw new Error(`Name is ambiguous. Use an entity ID: ${matches.map((item) => item.id).join(', ')}`);
if (!matches.length) throw new Error('No activity item matches that name or entity ID');
if (!matches.length) throw new Error('No activity control item matches that name or entity ID');
return matches[0];
}
@@ -52,14 +52,14 @@ module.exports = {
],
},
schema: strictObject({
enabled: boolean({ description: 'Immediately connects to Home Assistant and enables configured room entities, physical-button triggers, Neato controls, and lift controls.' }),
enabled: boolean({ description: 'Immediately connects to Home Assistant and enables configured room lights, physical-button triggers, Neato controls, and lift controls.' }),
url: string({ title: 'Server URL', description: 'Base URL of the Home Assistant server used for its REST and WebSocket APIs.', format: 'uri', maxLength: 2048 }),
token: string({ title: 'Long-lived access token', description: 'Home Assistant long-lived access token used to authenticate every API request. The saved value is never returned to the browser.', examples: ['REPLACE_WITH_LONG_LIVED_TOKEN'], writeOnly: true, maxLength: 20000 }),
[neato.key]: neato.schema,
[lift.key]: lift.schema,
entities: {
type: 'array',
title: 'Room entities',
title: 'Room Lights',
description: 'Home Assistant lights and switches exposed to the room-light controls and button-box actions.',
items: strictObject({
id: string({ title: 'Entity id', description: 'Exact Home Assistant entity ID, such as light.rover_room or switch.floor_lamp.', examples: ['light.lab_main'], minLength: 1, maxLength: 255 }),
@@ -55,7 +55,7 @@ function registerHomeAssistantHooks(deps) {
return cb({ error: 'Insufficient permissions to control Home Assistant' });
}
if (isBlockedByRoomControlLock()) {
return cb({ error: 'Room controls are locked' });
return cb({ error: 'Room lights are locked' });
}
try {
if (!entityId) throw new Error('entityId required');
@@ -71,7 +71,7 @@ function registerHomeAssistantHooks(deps) {
return cb({ error: 'Insufficient permissions to control Home Assistant' });
}
if (isBlockedByRoomControlLock()) {
return cb({ error: 'Room controls are locked' });
return cb({ error: 'Room lights are locked' });
}
try {
if (!entityId) throw new Error('entityId required');
@@ -87,7 +87,7 @@ function registerHomeAssistantHooks(deps) {
return cb({ error: 'Insufficient permissions to control Home Assistant' });
}
if (isBlockedByRoomControlLock()) {
return cb({ error: 'Room controls are locked' });
return cb({ error: 'Room lights are locked' });
}
try {
if (!entityId) throw new Error('entityId required');
@@ -106,7 +106,7 @@ function registerHomeAssistantHooks(deps) {
return cb({ error: 'Insufficient permissions to control Home Assistant' });
}
if (isBlockedByRoomControlLock()) {
return cb({ error: 'Room controls are locked' });
return cb({ error: 'Room lights are locked' });
}
try {
if (!entityId) throw new Error('entityId required');
+1 -1
View File
@@ -19,7 +19,7 @@ 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', {
logger.info('Idle room-controls off skipped because room lights are locked on', {
lockState,
source: 'idleService:turnOffRoomControls',
});
@@ -5,12 +5,12 @@ const { getCommandConfig } = require('../config');
function createHaCommand({ homeAssistantActivitiesService: service, config }) {
return async function handleHaCommand(message, tokens = []) {
const reply = (content) => message.reply({ content, allowedMentions: { parse: [], repliedUser: false } });
if (!service) return reply('Home Assistant activities are unavailable.');
if (!service) return reply('Activity Controls are unavailable.');
const action = String(tokens[0] || 'status').toLowerCase();
try {
if (action === 'status') {
const items = service.getState().items;
return reply(items.length ? items.map((item) => `${item.name} (${item.id}): ${item.locked ? 'locked' : 'unlocked'}`).join('\n') : 'No activity items configured.');
return reply(items.length ? items.map((item) => `${item.name} (${item.id}): ${item.locked ? 'locked' : 'unlocked'}`).join('\n') : 'No activity control items configured.');
}
if (!['lock', 'unlock'].includes(action) || tokens.length < 2) {
return reply(`Use ${getCommandConfig(config).prefix} ha status, or ha <lock|unlock> <name or entity ID>.`);
@@ -24,7 +24,7 @@ function buildCommandRegistry(prefix, timeCommand) {
// integration is absent.
green: { category: 'admin', summary: 'Toggle green room and page mode.', usage: [`${prefix} green <on|off>`], access: 'Admin', permission: 'admin' },
// Locks are moderation only; entity actions remain in the Activities card.
ha: { category: 'features', summary: 'List activity locks or lock/unlock an item by name or entity ID.', usage: [`${prefix} ha status`, `${prefix} ha <lock|unlock> <name or entity ID>`], access: 'Admin', permission: 'admin', requiredFeature: 'homeAssistantActivities', unavailableLabel: 'Home Assistant activities' },
ha: { category: 'features', summary: 'List activity control locks or lock/unlock an item by name or entity ID.', usage: [`${prefix} ha status`, `${prefix} ha <lock|unlock> <name or entity ID>`], access: 'Admin', permission: 'admin', requiredFeature: 'homeAssistantActivities', unavailableLabel: 'Activity Controls' },
lights: {
category: 'features',
summary: 'Control room lights or manage the admin light lock.',