mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
green mode slop 1
This commit is contained in:
File diff suppressed because one or more lines are too long
+5
-5
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -12,8 +12,8 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<!-- site-metadata:inject -->
|
||||
<!-- analytics:inject -->
|
||||
<script type="module" crossorigin src="/assets/index-D8T7esRs.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-7PpZTwSc.css">
|
||||
<script type="module" crossorigin src="/assets/index-Bp0VsaHN.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DKoVL79I.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Reward Definition: Green Mode
|
||||
// Purpose: Enables the server-wide green theme and room effect for twenty minutes.
|
||||
// Scope: Owns button-box timing/recovery while delegating the actual mode to greenModeService.
|
||||
const DURATION_MS = 20 * 60 * 1000;
|
||||
|
||||
let activeTimer = null;
|
||||
let unsubscribeGreenMode = null;
|
||||
|
||||
function clearRuntimeWatchers() {
|
||||
if (activeTimer) {
|
||||
clearTimeout(activeTimer);
|
||||
activeTimer = null;
|
||||
}
|
||||
if (unsubscribeGreenMode) {
|
||||
unsubscribeGreenMode();
|
||||
unsubscribeGreenMode = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function stopGreenMode(ctx) {
|
||||
clearRuntimeWatchers();
|
||||
await ctx.setGreenMode(false, { source: 'buttonbox:greenModeExpired' });
|
||||
ctx.clearEffect('greenMode');
|
||||
}
|
||||
|
||||
async function startGreenMode(ctx, effect = {}) {
|
||||
clearRuntimeWatchers();
|
||||
const endsAt = Number(effect.endsAt || Date.now() + DURATION_MS);
|
||||
const remaining = Math.max(0, endsAt - Date.now());
|
||||
|
||||
if (remaining <= 0) {
|
||||
await stopGreenMode(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.setGreenMode(true, { source: 'buttonbox:greenMode' });
|
||||
ctx.saveEffect('greenMode', { endsAt });
|
||||
|
||||
/*
|
||||
Access-mode changes disable green mode through greenModeService. Watching
|
||||
that shared state transition lets the reward discard its persisted effect
|
||||
immediately, so a restart cannot accidentally revive a reward that was
|
||||
intentionally ended early.
|
||||
*/
|
||||
unsubscribeGreenMode = ctx.onGreenModeChange((enabled) => {
|
||||
if (enabled) return;
|
||||
clearRuntimeWatchers();
|
||||
ctx.clearEffect('greenMode');
|
||||
});
|
||||
|
||||
activeTimer = setTimeout(() => {
|
||||
stopGreenMode(ctx).catch((err) => {
|
||||
ctx.logger.warn('green mode reward stop failed', { error: err.message });
|
||||
});
|
||||
}, remaining);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
id: 'greenMode',
|
||||
name: 'Green mode',
|
||||
description: 'Makes the room and server green for 20 minutes.',
|
||||
goal: 5,
|
||||
async run(ctx) {
|
||||
await startGreenMode(ctx, { endsAt: Date.now() + DURATION_MS });
|
||||
},
|
||||
async recover(ctx, effect) {
|
||||
// Recovery must never manufacture a fresh twenty-minute window from a
|
||||
// missing or corrupt persisted deadline. Treat it as expired and clean up.
|
||||
if (!Number.isFinite(Number(effect?.endsAt))) {
|
||||
await stopGreenMode(ctx);
|
||||
return;
|
||||
}
|
||||
await startGreenMode(ctx, effect);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
// Green Mode Reward Tests
|
||||
// Purpose: Pins the five-press metadata and persisted timed-effect lifecycle.
|
||||
// Scope: Uses a small context double; greenModeService behavior is tested through its public contract.
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const reward = require('./greenMode');
|
||||
|
||||
function createContext() {
|
||||
const calls = [];
|
||||
let changeListener = null;
|
||||
return {
|
||||
calls,
|
||||
logger: { warn: () => {} },
|
||||
setGreenMode: async (enabled, options) => {
|
||||
calls.push({ type: 'set', enabled, source: options?.source });
|
||||
return enabled;
|
||||
},
|
||||
saveEffect: (id, payload) => calls.push({ type: 'save', id, payload }),
|
||||
clearEffect: (id) => calls.push({ type: 'clear', id }),
|
||||
onGreenModeChange: (listener) => {
|
||||
changeListener = listener;
|
||||
return () => {
|
||||
changeListener = null;
|
||||
};
|
||||
},
|
||||
emitGreenModeChange: (enabled) => changeListener?.(enabled),
|
||||
};
|
||||
}
|
||||
|
||||
test('green mode reward requires five presses and starts a persisted effect', async () => {
|
||||
const ctx = createContext();
|
||||
assert.equal(reward.goal, 5);
|
||||
|
||||
await reward.run(ctx);
|
||||
|
||||
assert.deepEqual(ctx.calls[0], { type: 'set', enabled: true, source: 'buttonbox:greenMode' });
|
||||
const saved = ctx.calls.find((call) => call.type === 'save');
|
||||
assert.equal(saved?.id, 'greenMode');
|
||||
assert.ok(saved?.payload?.endsAt > Date.now());
|
||||
|
||||
// Simulate an access-mode shutdown so the test also clears the reward's
|
||||
// twenty-minute timer instead of leaving background work in the test process.
|
||||
ctx.emitGreenModeChange(false);
|
||||
assert.ok(ctx.calls.some((call) => call.type === 'clear' && call.id === 'greenMode'));
|
||||
});
|
||||
|
||||
test('invalid recovery state is cleared instead of starting a new duration', async () => {
|
||||
const ctx = createContext();
|
||||
await reward.recover(ctx, {});
|
||||
|
||||
assert.deepEqual(ctx.calls[0], {
|
||||
type: 'set',
|
||||
enabled: false,
|
||||
source: 'buttonbox:greenModeExpired',
|
||||
});
|
||||
assert.ok(ctx.calls.some((call) => call.type === 'clear' && call.id === 'greenMode'));
|
||||
});
|
||||
@@ -10,6 +10,7 @@ const discordPingEveryone = require('./definitions/discordPingEveryone');
|
||||
const modeJam = require('./definitions/modeJam');
|
||||
const assignmentRoulette = require('./definitions/assignmentRoulette');
|
||||
const chatSpam = require('./definitions/chatSpam');
|
||||
const greenMode = require('./definitions/greenMode');
|
||||
|
||||
const orderedRewards = [
|
||||
dockPanic,
|
||||
@@ -22,6 +23,7 @@ const orderedRewards = [
|
||||
modeJam,
|
||||
assignmentRoulette,
|
||||
chatSpam,
|
||||
greenMode,
|
||||
];
|
||||
|
||||
const rewardById = new Map(orderedRewards.map((reward, idx) => [reward.id, { ...reward, number: idx + 1 }]));
|
||||
|
||||
@@ -22,6 +22,9 @@ function createButtonBoxCore(deps) {
|
||||
getHomeAssistantState,
|
||||
setHomeAssistantEntityState,
|
||||
setHomeAssistantLightsLockedOn,
|
||||
setGreenMode,
|
||||
isGreenModeEnabled,
|
||||
onGreenModeChange,
|
||||
store,
|
||||
} = deps;
|
||||
|
||||
@@ -215,6 +218,11 @@ function createButtonBoxCore(deps) {
|
||||
setHomeAssistantEntityState(entityId, state, { source: 'buttonBoxReward' }),
|
||||
setHomeAssistantLightsLockedOn: (next, options = {}) =>
|
||||
setHomeAssistantLightsLockedOn(next, options),
|
||||
// Rewards receive the standalone feature boundary rather than reaching
|
||||
// into Home Assistant or duplicating green-mode state and alerts.
|
||||
setGreenMode: (next, options = {}) => setGreenMode(next, options),
|
||||
isGreenModeEnabled: () => isGreenModeEnabled(),
|
||||
onGreenModeChange: (listener) => onGreenModeChange(listener),
|
||||
saveEffect: (effectId, payload = {}) => saveEffect(effectId, payload, { broadcast: false }),
|
||||
clearEffect: (effectId) => clearEffect(effectId, { broadcast: false }),
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ const { isLocalNetwork, normalizeIp } = require('../../helpers/ipResolver');
|
||||
const { createButtonBoxStore } = require('./store');
|
||||
const { createButtonBoxCore } = require('./core');
|
||||
const { registerButtonBoxRoute } = require('./httpRoute');
|
||||
const greenModeService = require('../greenModeService');
|
||||
|
||||
const DATA_DIR = resolveDataDir();
|
||||
const STORE_PATH = resolveDataPath('buttonbox-state.json');
|
||||
@@ -61,6 +62,14 @@ const core = createButtonBoxCore({
|
||||
getHomeAssistantState,
|
||||
setHomeAssistantEntityState,
|
||||
setHomeAssistantLightsLockedOn,
|
||||
setGreenMode: greenModeService.setEnabled,
|
||||
isGreenModeEnabled: greenModeService.isEnabled,
|
||||
// Return an explicit cleanup function so timed rewards can stop observing
|
||||
// the global service when they expire, rerun, or are recovered.
|
||||
onGreenModeChange: (listener) => {
|
||||
greenModeService.greenModeEvents.on('change', listener);
|
||||
return () => greenModeService.greenModeEvents.off('change', listener);
|
||||
},
|
||||
store,
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ const { getNickname } = require('../nicknameService');
|
||||
const { getGlobalObjective, setGlobalObjective, clearGlobalObjective } = require('../globalObjectiveService');
|
||||
const { getAdminReason, setAdminReason, clearAdminReason } = require('../adminReasonService');
|
||||
const homeAssistantService = require('../homeAssistantService');
|
||||
const greenModeService = require('../greenModeService');
|
||||
const liftService = require('../liftService');
|
||||
const neatoService = require('../neatoService');
|
||||
const { isFeatureEnabled } = require('../../helpers/features');
|
||||
@@ -174,6 +175,7 @@ async function runChatTextCommand({ text, socket, sendSystemMessage }) {
|
||||
// lights lock/unlock` from becoming transport-specific, and it preserves
|
||||
// the existing session update path for all connected browsers.
|
||||
homeAssistantService,
|
||||
greenModeService,
|
||||
liftService,
|
||||
neatoService,
|
||||
isFeatureEnabled,
|
||||
|
||||
@@ -63,6 +63,7 @@ const { subscribe } = require('../eventBus');
|
||||
const { createPresenceManager } = require('./presence');
|
||||
const { createChannelIO } = require('./channelIO');
|
||||
const { createCommandHandlers } = require('../operatorCommandService');
|
||||
const greenModeService = require('../greenModeService');
|
||||
const { createDiscordTransportHandlers, createDiscordCommandRequest } = require('./commandAdapter');
|
||||
const { createIntegrations } = require('./integrations');
|
||||
const { createFleetDailyReports } = require('./fleetDailyReports');
|
||||
@@ -240,6 +241,7 @@ const commandDependencies = {
|
||||
// service into the shared command router keeps Discord and mirrored web-chat
|
||||
// command behavior aligned without duplicating Home Assistant calls here.
|
||||
homeAssistantService,
|
||||
greenModeService,
|
||||
liftService,
|
||||
neatoService,
|
||||
isFeatureEnabled,
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// Green Mode Service
|
||||
// Purpose: Owns the temporary server-wide green visual mode and its tiny light workflow.
|
||||
// Scope: Composes existing Home Assistant operations; it does not add policy to that service.
|
||||
const EventEmitter = require('events');
|
||||
const logger = require('../../globals/logger').child('greenModeService');
|
||||
const { sendAlert } = require('../alertService');
|
||||
const homeAssistantService = require('../homeAssistantService');
|
||||
const { modeEvents } = require('../modeManager');
|
||||
|
||||
const GREEN_MODE_COLOR = '#00ff00';
|
||||
const greenModeEvents = new EventEmitter();
|
||||
let enabled = false;
|
||||
|
||||
function isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
async function setEnabled(nextValue, options = {}) {
|
||||
const next = Boolean(nextValue);
|
||||
if (enabled === next) return enabled;
|
||||
|
||||
if (next) {
|
||||
/*
|
||||
Lock first because the existing locked-on transition sets lights white.
|
||||
Recoloring RGB lights afterward leaves them green while retaining the
|
||||
established room-control lock, idle protection, and laser safety rules.
|
||||
*/
|
||||
await homeAssistantService.setLightsLockedOn(true, {
|
||||
source: String(options?.source || 'greenMode:enable'),
|
||||
});
|
||||
|
||||
const entities = homeAssistantService.getState()?.entities || [];
|
||||
/*
|
||||
RGB-capable lights become the requested solid green. Every other
|
||||
configured room control, including white-only bulbs and switches, is
|
||||
explicitly turned off so the physical room has one unambiguous effect.
|
||||
These remain generic Home Assistant calls; that service does not know
|
||||
that the operations belong to green mode.
|
||||
*/
|
||||
const results = await Promise.allSettled(
|
||||
entities.map((entity) => (
|
||||
entity?.supportsColor
|
||||
? homeAssistantService.setLightColor(entity.id, GREEN_MODE_COLOR)
|
||||
: homeAssistantService.setEntityState(entity.id, 'off', {
|
||||
source: 'greenMode:non-rgb-off',
|
||||
})
|
||||
)),
|
||||
);
|
||||
const failures = results
|
||||
.map((result, index) => ({ result, entityId: entities[index].id }))
|
||||
.filter(({ result }) => result.status === 'rejected')
|
||||
.map(({ result, entityId }) => ({ entityId, error: result.reason?.message || 'unknown error' }));
|
||||
|
||||
if (failures.length) {
|
||||
logger.warn('Some room controls failed to enter green mode', { failures });
|
||||
}
|
||||
} else {
|
||||
// Disabling the visual mode simply releases the lock it created. Bulb
|
||||
// colors remain untouched, matching the existing one-shot light behavior.
|
||||
await homeAssistantService.setLightsLockedOn(false, {
|
||||
source: String(options?.source || 'greenMode:disable'),
|
||||
});
|
||||
}
|
||||
|
||||
enabled = next;
|
||||
logger.info('Green mode changed', {
|
||||
enabled,
|
||||
source: options?.source || 'unknown',
|
||||
});
|
||||
// Emit one shared server alert for every completed transition. Automatic
|
||||
// access-mode shutdown uses this same function, so clients also receive the
|
||||
// inactive notice when green mode ends without an explicit chat command.
|
||||
sendAlert({
|
||||
color: GREEN_MODE_COLOR,
|
||||
title: 'Green mode',
|
||||
message: enabled ? 'Green mode is active.' : 'Green mode is inactive.',
|
||||
});
|
||||
greenModeEvents.emit('change', enabled);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
modeEvents.on('change', () => {
|
||||
if (!enabled) return;
|
||||
setEnabled(false, { source: 'modeGateReset' }).catch((err) => {
|
||||
logger.warn('Failed to disable green mode on access-mode change', err.message);
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
isEnabled,
|
||||
setEnabled,
|
||||
greenModeEvents,
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
// Operator Green Command
|
||||
// Purpose: Toggles the intentionally silly server-wide green visual and room-light mode.
|
||||
// Scope: Keeps command presentation here while Home Assistant owns the runtime policy.
|
||||
const { getCommandConfig } = require('../../operatorCommandService/config');
|
||||
|
||||
function createGreenCommand({ greenModeService, sanitizeMentions, config }) {
|
||||
const { prefix: commandPrefix } = getCommandConfig(config);
|
||||
|
||||
return async function handleGreenCommand(message, tokens = []) {
|
||||
const action = String(tokens.shift() || '').trim().toLowerCase();
|
||||
if (action !== 'on' && action !== 'off') {
|
||||
await message.reply({
|
||||
content: `Invalid green command. Use \`${commandPrefix} green on\` or \`${commandPrefix} green off\`.`,
|
||||
allowedMentions: { parse: [], repliedUser: false },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const enabled = action === 'on';
|
||||
const result = await greenModeService.setEnabled(enabled, {
|
||||
source: `bot-command:green:${action}`,
|
||||
});
|
||||
await message.reply({
|
||||
content: sanitizeMentions(result ? 'Green mode enabled.' : 'Green mode disabled.'),
|
||||
allowedMentions: { parse: [], repliedUser: false },
|
||||
});
|
||||
} catch (err) {
|
||||
await message.reply({
|
||||
content: sanitizeMentions(`Failed to update green mode: ${err.message}`),
|
||||
allowedMentions: { parse: [], repliedUser: false },
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { createGreenCommand };
|
||||
@@ -10,6 +10,7 @@ const { createVerifyCommand } = require('./commands/verify');
|
||||
const { createDeterCommand } = require('./commands/deter');
|
||||
const { createPermissionsCommand } = require('./commands/permissions');
|
||||
const { createLightsCommand } = require('./commands/lights');
|
||||
const { createGreenCommand } = require('./commands/green');
|
||||
const { createKickCommand } = require('./commands/kick');
|
||||
const { createLiftCommand } = require('./commands/lift');
|
||||
const { createNeatoCommand } = require('./commands/neato');
|
||||
@@ -61,6 +62,7 @@ function createCommandHandlers(deps) {
|
||||
const handleBridgeCommand = transportHandlers.bridge;
|
||||
const handleTimeStatusCommand = transportHandlers.timeStatus;
|
||||
const handleLightsCommand = createLightsCommand(deps);
|
||||
const handleGreenCommand = createGreenCommand(deps);
|
||||
const handleKickCommand = createKickCommand(deps);
|
||||
const handleLiftCommand = createLiftCommand(deps);
|
||||
const handleNeatoCommand = createNeatoCommand(deps);
|
||||
@@ -108,7 +110,7 @@ function createCommandHandlers(deps) {
|
||||
// is included because its lock/unlock subcommands change room policy. Its
|
||||
// ordinary on/off/color actions are also intentionally restricted to a
|
||||
// lockdown admin while the entire server is in lockdown.
|
||||
const moderationActions = new Set(['lock', 'unlock', 'mode', 'goal', 'reason', 'verify', 'deter', 'permissions', 'lights', 'kick', 'lift', 'neato']);
|
||||
const moderationActions = new Set(['lock', 'unlock', 'mode', 'goal', 'reason', 'verify', 'deter', 'permissions', 'lights', 'green', 'kick', 'lift', 'neato']);
|
||||
const isAccessModeCommand = commandDefinition?.permission === 'access-mode';
|
||||
|
||||
// Feature commands are public activities while access is open or managed
|
||||
@@ -144,6 +146,8 @@ function createCommandHandlers(deps) {
|
||||
return handleBridgeCommand(request, tokens);
|
||||
case 'lights':
|
||||
return handleLightsCommand(request, tokens);
|
||||
case 'green':
|
||||
return handleGreenCommand(request, tokens);
|
||||
case 'kick':
|
||||
return handleKickCommand(request, rest);
|
||||
case 'lift':
|
||||
|
||||
@@ -30,6 +30,9 @@ function createRouter({ mode = MODES.OPEN, featureEnabled = true } = {}) {
|
||||
isFeatureEnabled: () => featureEnabled,
|
||||
sanitizeMentions: (text) => String(text || ''),
|
||||
homeAssistantService: { getLightPolicyState: () => ({}), setAllControllableEntitiesState: () => Promise.resolve() },
|
||||
// Green mode is admin-only at the dispatcher, so this double only needs to
|
||||
// prove that an authorized command reaches the small standalone service.
|
||||
greenModeService: { setEnabled: (enabled) => Promise.resolve(Boolean(enabled)) },
|
||||
liftService: null,
|
||||
neatoService: null,
|
||||
listVerifiedUsers: () => [],
|
||||
@@ -72,7 +75,7 @@ const admin = { id: 's1', userId: 'u-alice', label: 'alice', isAdmin: true, isLo
|
||||
|
||||
test('admin-only commands stay admin-only for a non-admin', async () => {
|
||||
const run = createRouter();
|
||||
for (const command of ['rs lock rover-1', 'rs unlock rover-1', 'rs mode open', 'rs kick alice', 'rs permissions list']) {
|
||||
for (const command of ['rs lock rover-1', 'rs unlock rover-1', 'rs mode open', 'rs green on', 'rs kick alice', 'rs permissions list']) {
|
||||
assert.match(await run(command, nonAdmin), ADMIN_DENIAL, `${command} must stay admin-only`);
|
||||
}
|
||||
});
|
||||
@@ -100,11 +103,22 @@ test('admin mode restricts access-mode feature commands', async () => {
|
||||
|
||||
test('lockdown still suspends the pre-existing moderation-sensitive commands', async () => {
|
||||
const run = createRouter({ mode: MODES.LOCKDOWN });
|
||||
for (const command of ['rs lock rover-1', 'rs mode open', 'rs lights on', 'rs goal', 'rs kick alice']) {
|
||||
for (const command of ['rs lock rover-1', 'rs mode open', 'rs lights on', 'rs green on', 'rs goal', 'rs kick alice']) {
|
||||
assert.match(await run(command, admin), LOCKDOWN_DENIAL, `${command} should stay lockdown-gated`);
|
||||
}
|
||||
});
|
||||
|
||||
test('admins can toggle green mode through the shared command path', async () => {
|
||||
const run = createRouter();
|
||||
assert.match(await run('rs green on', admin), /Green mode enabled/);
|
||||
assert.match(await run('rs green off', admin), /Green mode disabled/);
|
||||
});
|
||||
|
||||
test('green mode rejects unknown actions with focused usage guidance', async () => {
|
||||
const run = createRouter();
|
||||
assert.match(await run('rs green maybe', admin), /rs green on.*rs green off/);
|
||||
});
|
||||
|
||||
test('status and help survive lockdown', async () => {
|
||||
const run = createRouter({ mode: MODES.LOCKDOWN });
|
||||
assert.match(await run('rs status', nonAdmin), /\[status handler\]/);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Scope: Supplies transport-neutral metadata; execution handlers remain focused on server operations.
|
||||
const CATEGORIES = {
|
||||
system: { title: 'System', names: ['help', 'status', 'replay', 'time-status'] },
|
||||
admin: { title: 'Admin', names: ['lock', 'unlock', 'mode', 'reason', 'goal', 'kick', 'verify', 'deter', 'permissions'] },
|
||||
admin: { title: 'Admin', names: ['lock', 'unlock', 'mode', 'reason', 'goal', 'green', 'kick', 'verify', 'deter', 'permissions'] },
|
||||
features: { title: 'Features', names: ['lights', 'lift', 'neato'] },
|
||||
discord: { title: 'Discord', names: ['bridge'] },
|
||||
};
|
||||
@@ -19,6 +19,7 @@ function buildCommandRegistry(prefix, timeCommand) {
|
||||
mode: { category: 'admin', summary: 'Change the server mode.', usage: [`${prefix} mode <open|turns|admin|lockdown>`], access: 'Admin', permission: 'admin' },
|
||||
reason: { category: 'admin', summary: 'Show, set, or clear the admin-mode reason.', usage: [`${prefix} reason [text|clear]`], access: 'Admin to change' },
|
||||
goal: { category: 'admin', summary: 'Show, set, or clear the global objective.', usage: [`${prefix} goal [text|clear]`], access: 'Admin to change' },
|
||||
green: { category: 'admin', summary: 'Toggle green room and page mode.', usage: [`${prefix} green <on|off>`], access: 'Admin', permission: 'admin', requiredFeature: 'homeAssistant', unavailableLabel: 'Home Assistant' },
|
||||
lights: {
|
||||
category: 'features',
|
||||
summary: 'Control room lights or manage the admin light lock.',
|
||||
|
||||
@@ -17,6 +17,7 @@ const {
|
||||
ptzCameraEvents,
|
||||
} = require('../ptzCameraService');
|
||||
const { getState: getHomeAssistantState, homeAssistantEvents } = require('../homeAssistantService');
|
||||
const { isEnabled: isGreenModeEnabled, greenModeEvents } = require('../greenModeService');
|
||||
const { getState: getNeatoState, neatoEvents } = require('../neatoService');
|
||||
const { getState: getLiftState, liftEvents } = require('../liftService');
|
||||
const { getState: getKinectState, kinectEvents } = require('../kinectService');
|
||||
@@ -199,6 +200,9 @@ function buildSession(socket) {
|
||||
roomCameras: getRoomCameras(),
|
||||
ptzCamera: getPtzCameraState(socket),
|
||||
homeAssistant: getHomeAssistantState(),
|
||||
// Green mode is a server-wide visual feature. It stays separate from Home
|
||||
// Assistant state because HA only supplies the generic light operations.
|
||||
greenMode: isGreenModeEnabled(),
|
||||
neato: getNeatoState(),
|
||||
lift: getLiftState(),
|
||||
kinect: getKinectState(),
|
||||
@@ -391,6 +395,11 @@ homeAssistantEvents.on('update', () => {
|
||||
syncAll();
|
||||
});
|
||||
|
||||
greenModeEvents.on('change', () => {
|
||||
logger.info('Green mode change detected; syncing all clients');
|
||||
syncAll();
|
||||
});
|
||||
|
||||
homeAssistantEvents.on('status', () => {
|
||||
logger.info('Home Assistant status change; syncing all clients');
|
||||
syncAll();
|
||||
|
||||
Reference in New Issue
Block a user