mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 17:40:46 -04:00
rename old room controls to lights and rename new thing to activity controls
Container image / image (push) Failing after 1m2s
Container image / image (push) Failing after 1m2s
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -68,8 +68,8 @@ export const ACTIONS = [
|
||||
{ id: 'videoFilterCycle', label: 'Cycle video filter', kind: 'button', section: 'Camera' },
|
||||
{ id: 'songNoteUp', label: 'Play higher note', kind: 'button', section: 'Audio and chat', driveMode: 'single' },
|
||||
{ id: 'songNoteDown', label: 'Play lower note', kind: 'button', section: 'Audio and chat', driveMode: 'single' },
|
||||
{ id: 'homeAssistantOn', label: 'Turn next room control on', kind: 'button', section: 'Room controls' },
|
||||
{ id: 'homeAssistantOff', label: 'Turn next room control off', kind: 'button', section: 'Room controls' },
|
||||
{ id: 'homeAssistantOn', label: 'Turn next room light on', kind: 'button', section: 'Room lights' },
|
||||
{ id: 'homeAssistantOff', label: 'Turn next room light off', kind: 'button', section: 'Room lights' },
|
||||
/* Digital aux actions provide exact parity with the keyboard help surface. They coexist with
|
||||
analog brush controls so each operator can choose proportional triggers or discrete buttons. */
|
||||
{ id: 'auxMainForward', label: 'Main brush forward', kind: 'button', section: 'Aux buttons' },
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function HomeAssistantActivitiesPanel() {
|
||||
const connected = state.connected && socketConnected;
|
||||
// Reuse the room-control auto-fit grid and compact tile rhythm, while keeping
|
||||
// all data and commands in the Activities namespace on both device layouts.
|
||||
return <CardFrame title="Home Assistant" bodyClassName="space-y-0.5 text-sm"
|
||||
return <CardFrame title="Activity Controls" bodyClassName="space-y-0.5 text-sm"
|
||||
actions={<span className={`rounded px-1 py-0.5 text-xs font-semibold leading-none ${connected ? 'bg-emerald-900 text-emerald-100' : 'bg-amber-900 text-amber-100'}`}>{connected ? 'Connected' : 'Offline'}</span>}>
|
||||
{!connected ? <p className="px-0.5 text-xs text-amber-200">{socketConnected ? 'Home Assistant is offline.' : 'Server disconnected.'} Values may be out of date.</p> : null}
|
||||
{!allowed ? <p className="px-0.5 text-xs text-slate-400">Controls are read-only with your current access.</p> : null}
|
||||
|
||||
@@ -174,7 +174,7 @@ export default function HomeAssistantControls() {
|
||||
|
||||
/*
|
||||
Feature existence is owned here, not by each layout that happens to mount
|
||||
room controls. Disabled integrations render nothing; enabled integrations
|
||||
room lights. Disabled integrations render nothing; enabled integrations
|
||||
can still show offline/configuration states inside the panel.
|
||||
*/
|
||||
if (!enabled) return null;
|
||||
@@ -199,7 +199,7 @@ function HomeAssistantControlsContent() {
|
||||
|
||||
if (!ha?.enabled) {
|
||||
return (
|
||||
<CardFrame title="Room Controls" bodyClassName="space-y-0.5 text-sm text-slate-400">
|
||||
<CardFrame title="Room Lights" bodyClassName="space-y-0.5 text-sm text-slate-400">
|
||||
<p className="text-slate-500">Not configured on the server.</p>
|
||||
</CardFrame>
|
||||
);
|
||||
@@ -207,7 +207,7 @@ function HomeAssistantControlsContent() {
|
||||
|
||||
if (entities.length === 0) {
|
||||
return (
|
||||
<CardFrame title="Room Controls" bodyClassName="space-y-0.5 text-sm text-slate-400">
|
||||
<CardFrame title="Room Lights" bodyClassName="space-y-0.5 text-sm text-slate-400">
|
||||
<p className="text-slate-500">No lights or switches configured.</p>
|
||||
</CardFrame>
|
||||
);
|
||||
@@ -233,16 +233,16 @@ function HomeAssistantControlsContent() {
|
||||
);
|
||||
|
||||
return (
|
||||
<CardFrame title="Room Controls" actions={actions} bodyClassName="space-y-0.5 text-base">
|
||||
<CardFrame title="Room Lights" actions={actions} bodyClassName="space-y-0.5 text-base">
|
||||
{lightPolicyLocked ? (
|
||||
<p className="rounded border border-amber-600/60 bg-amber-900/40 px-1 py-0.5 text-xs text-amber-100">
|
||||
{adminCanControlLockedLights
|
||||
? lockState === 'off'
|
||||
? 'Lights are locked off. Admin room controls remain available.'
|
||||
: 'Lights are locked on. Admin room controls remain available.'
|
||||
? 'Lights are locked off. Admin room lights remain available.'
|
||||
: 'Lights are locked on. Admin room lights remain available.'
|
||||
: lockState === 'off'
|
||||
? 'Lights are locked off. Room controls are disabled.'
|
||||
: 'Lights are locked on. Room controls are disabled.'}
|
||||
? 'Lights are locked off. Room lights are disabled.'
|
||||
: 'Lights are locked on. Room lights are disabled.'}
|
||||
</p>
|
||||
) : null}
|
||||
{/* Each lamp declares only the smallest width at which its title, status,
|
||||
|
||||
@@ -36,8 +36,8 @@ const KEY_ACTIONS = [
|
||||
{ id: 'chatFocus', label: 'Toggle Chat', group: 'Chat' },
|
||||
{ id: 'songNoteUp', label: 'Song Note Up', group: 'Audio' },
|
||||
{ id: 'songNoteDown', label: 'Song Note Down', group: 'Audio' },
|
||||
{ id: 'homeAssistantOn', label: 'Room Controls On (Cycle)', group: 'Room Controls' },
|
||||
{ id: 'homeAssistantOff', label: 'Room Controls Off (Cycle)', group: 'Room Controls' },
|
||||
{ id: 'homeAssistantOn', label: 'Room Lights On (Cycle)', group: 'Room Lights' },
|
||||
{ id: 'homeAssistantOff', label: 'Room Lights Off (Cycle)', group: 'Room Lights' },
|
||||
];
|
||||
|
||||
function groupActions(actions) {
|
||||
|
||||
@@ -556,7 +556,7 @@ function PtzDesktopFullscreen({ ptz, releasePending }) {
|
||||
<PtzControlReference />
|
||||
<ReplaySourcesPanel panelId="ptz-controller-replay" defaultSelectedKey={`ptz:${PTZ_CAMERA_ID}`} />
|
||||
{/*
|
||||
Desktop keeps room controls as the final sidebar tool so camera
|
||||
Desktop keeps room lights as the final sidebar tool so camera
|
||||
turn controls and replay remain above the less-frequent room-wide
|
||||
actions. HomeAssistantControls owns its own feature and policy gate.
|
||||
*/}
|
||||
@@ -604,7 +604,7 @@ function PtzMobileLandscape({ ptz, onClose, releasePending = false }) {
|
||||
</main>
|
||||
{/*
|
||||
The right control column is naturally taller than the viewport.
|
||||
Placing room controls after the fixed-height video uses that left-
|
||||
Placing room lights after the fixed-height video uses that left-
|
||||
column space while the whole landscape page continues scrolling as
|
||||
one surface.
|
||||
*/}
|
||||
@@ -678,7 +678,7 @@ function PtzMobilePortrait({ ptz, onClose, releasePending = false }) {
|
||||
</div>
|
||||
<PtzQueueSummary ptz={ptz} />
|
||||
<ChatPanel title="Chat" allowSpectatorInput inputTarget="overlay" />
|
||||
{/* Portrait keeps room controls immediately after chat as requested. */}
|
||||
{/* Portrait keeps room lights immediately after chat as requested. */}
|
||||
<HomeAssistantControls />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -189,7 +189,7 @@ export function ControlSystemProvider({ children }) {
|
||||
Automatic drive-mode lighting is convenience behavior for the open room.
|
||||
A room-light lock is an explicit policy decision, including locked-off,
|
||||
so this helper must not issue any Home Assistant commands while that
|
||||
policy is active. Admins can still use the dedicated room controls when
|
||||
policy is active. Admins can still use the dedicated room lights when
|
||||
they need to override individual lamps.
|
||||
*/
|
||||
if (roomLightsLocked) {
|
||||
|
||||
@@ -328,7 +328,7 @@ export default function KeyboardInputManager() {
|
||||
shortcuts are part of the public room-control surface. Admin sessions are
|
||||
allowed through when the current site mode would also allow their socket
|
||||
command, so keyboard behavior matches the server-side authorization and
|
||||
the clickable Room Controls panel.
|
||||
the clickable Room Lights panel.
|
||||
*/
|
||||
if ((ha?.lightPolicy?.locked || ha?.lightPolicy?.lockedOn) && !latest?.adminCanControlLockedLights) return;
|
||||
const entities = ha.entities || [];
|
||||
|
||||
@@ -112,10 +112,10 @@ export const HELP_CONTENT = {
|
||||
},
|
||||
{
|
||||
id: 'room-controls',
|
||||
title: 'Room Controls',
|
||||
title: 'Room Lights',
|
||||
items: [
|
||||
{ action: 'homeAssistantOn', label: 'Next room control on' },
|
||||
{ action: 'homeAssistantOff', label: 'Next room control off (reverse)' },
|
||||
{ action: 'homeAssistantOn', label: 'Next room light on' },
|
||||
{ action: 'homeAssistantOff', label: 'Next room light off (reverse)' },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -163,7 +163,7 @@ export const HELP_CONTENT = {
|
||||
type: 'list',
|
||||
title: 'More controls',
|
||||
items: [
|
||||
'Chat, Activities, VIP, Room Controls, Help, and Settings are below the rover controls.',
|
||||
'Chat, Activities, VIP, Room Lights, Help, and Settings are below the rover controls.',
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -200,7 +200,7 @@ export const HELP_CONTENT = {
|
||||
type: 'list',
|
||||
title: 'More controls',
|
||||
items: [
|
||||
'Chat, Activities, VIP, Room Controls, Help, and Settings are below the rover controls.',
|
||||
'Chat, Activities, VIP, Room Lights, Help, and Settings are below the rover controls.',
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function MobileTabs() {
|
||||
<Tab id="chat">Chat</Tab>
|
||||
<Tab id="activities">Activities</Tab>
|
||||
<VipTabButton compact />
|
||||
<Tab id="roomcontrols">Room Controls</Tab>
|
||||
<Tab id="roomcontrols">Room Lights</Tab>
|
||||
<Tab id="help">Help</Tab>
|
||||
<Tab id="settings">Settings</Tab>
|
||||
</TabList>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Mobile Room Controls Tab
|
||||
// Mobile Room Lights Tab
|
||||
// Purpose: Owns the concrete mobile room-controls card order.
|
||||
import { TabPanel } from '../../../../../components/Tabs/index.jsx';
|
||||
import HomeAssistantControls from '../../../../../components/HomeAssistantControls/index.jsx';
|
||||
|
||||
Reference in New Issue
Block a user