mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
chat history and other stuffs, light commands replay fixes
This commit is contained in:
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
@@ -78,7 +78,7 @@
|
||||
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
|
||||
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
|
||||
<title>Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-BlPOv38a.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-DY2RJqPm.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BwjDTdpq.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -78,6 +78,7 @@ module.exports = {
|
||||
setLightColor: runtimeEngine.setLightColor,
|
||||
setLightWhite: runtimeEngine.setLightWhite,
|
||||
setAllControllableEntitiesState: runtimeEngine.setAllControllableEntitiesState,
|
||||
setRandomColorScene: runtimeEngine.setRandomColorScene,
|
||||
setLightsLockedOn: runtimeEngine.setLightsLockedOn,
|
||||
toggleLightsLockedOn: runtimeEngine.toggleLightsLockedOn,
|
||||
homeAssistantEvents: events,
|
||||
|
||||
@@ -196,6 +196,72 @@ function createRuntimeEngine(deps) {
|
||||
};
|
||||
}
|
||||
|
||||
function createBrightRandomRgbColor() {
|
||||
// A completely random RGB triplet frequently produces colors that are very
|
||||
// dark, gray, or visually indistinguishable from a bulb being off. Choosing
|
||||
// a random hue at full saturation and brightness still gives every bulb a
|
||||
// genuinely random color while keeping the requested room effect vivid.
|
||||
const hueSegment = Math.random() * 6;
|
||||
const segmentIndex = Math.floor(hueSegment);
|
||||
const risingChannel = Math.round((hueSegment - segmentIndex) * 255);
|
||||
const fallingChannel = 255 - risingChannel;
|
||||
|
||||
switch (segmentIndex) {
|
||||
case 0: return [255, risingChannel, 0];
|
||||
case 1: return [fallingChannel, 255, 0];
|
||||
case 2: return [0, 255, risingChannel];
|
||||
case 3: return [0, fallingChannel, 255];
|
||||
case 4: return [risingChannel, 0, 255];
|
||||
default: return [255, 0, fallingChannel];
|
||||
}
|
||||
}
|
||||
|
||||
async function setRandomColorScene(options = {}) {
|
||||
const source = String(options?.source || 'homeAssistant:setRandomColorScene');
|
||||
const entities = Array.from(entityConfig.values()).map((meta) => ({
|
||||
meta,
|
||||
state: entityState.get(meta.id) || buildState(meta, null),
|
||||
}));
|
||||
|
||||
// RGB capability comes from Home Assistant's live supported_color_modes
|
||||
// snapshot. This avoids a second operator-maintained list and makes newly
|
||||
// replaced bulbs automatically participate once Home Assistant reports
|
||||
// their capabilities. Everything else is turned off, including switches
|
||||
// and white-only lights, exactly matching the scene's requested boundary.
|
||||
const operations = entities.map(({ meta, state }) => {
|
||||
if (state.supportsColor) {
|
||||
return setLightColor(meta.id, createBrightRandomRgbColor());
|
||||
}
|
||||
return setEntityState(meta.id, 'off', { source: `${source}:non-rgb-off` });
|
||||
});
|
||||
const results = await Promise.allSettled(operations);
|
||||
const failures = results
|
||||
.map((result, index) => ({ result, entityId: entities[index].meta.id }))
|
||||
.filter(({ result }) => result.status === 'rejected')
|
||||
.map(({ result, entityId }) => ({ entityId, error: result.reason?.message || 'unknown error' }));
|
||||
const succeeded = results
|
||||
.map((result, index) => ({ result, entityId: entities[index].meta.id }))
|
||||
.filter(({ result }) => result.status === 'fulfilled')
|
||||
.map(({ entityId }) => entityId);
|
||||
|
||||
if (failures.length) {
|
||||
logger.warn('Some Home Assistant random color scene updates failed', {
|
||||
total: entities.length,
|
||||
failed: failures.length,
|
||||
failures,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
source,
|
||||
total: entities.length,
|
||||
colorLights: entities.filter(({ state }) => state.supportsColor).length,
|
||||
nonColorEntities: entities.filter(({ state }) => !state.supportsColor).length,
|
||||
succeeded,
|
||||
failures,
|
||||
};
|
||||
}
|
||||
|
||||
async function setEntityLockedOnWhite(entityId, options = {}) {
|
||||
const meta = entityConfig.get(entityId);
|
||||
const source = String(options?.source || 'homeAssistant:setEntityLockedOnWhite');
|
||||
@@ -498,6 +564,7 @@ function createRuntimeEngine(deps) {
|
||||
setLightColor,
|
||||
setLightWhite,
|
||||
setAllControllableEntitiesState,
|
||||
setRandomColorScene,
|
||||
setAllControllableEntitiesLockedOnWhite,
|
||||
setLightsLockedOn,
|
||||
toggleLightsLockedOn,
|
||||
|
||||
@@ -33,6 +33,34 @@ function createLightsCommand({ homeAssistantService, sanitizeMentions, config })
|
||||
return;
|
||||
}
|
||||
|
||||
const isAdmin = Boolean(message.actor?.isAdmin);
|
||||
const adminActions = new Set(['status', 'lock', 'unlock']);
|
||||
|
||||
// The lights namespace intentionally contains both public feature actions
|
||||
// and room-policy actions. The shared dispatcher applies the current server
|
||||
// mode to the feature as a whole; this focused check preserves the stronger
|
||||
// historical permission on status/lock/unlock without making on/off/colors
|
||||
// admin-only during normal open or turns operation.
|
||||
if (adminActions.has(action) && !isAdmin) {
|
||||
await message.reply({
|
||||
content: 'Only admins can manage the room-light lock.',
|
||||
allowedMentions: { parse: [], repliedUser: false },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// An active lock is a policy boundary for ordinary feature commands. Admin
|
||||
// lock management remains available, but public scene commands must not
|
||||
// silently defeat a locked-on or locked-off room state.
|
||||
const lightPolicy = homeAssistantService.getLightPolicyState?.() || {};
|
||||
if ((action === 'on' || action === 'off' || action === 'colors') && lightPolicy.locked) {
|
||||
await message.reply({
|
||||
content: describeLightPolicy(lightPolicy),
|
||||
allowedMentions: { parse: [], repliedUser: false },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'status') {
|
||||
await message.reply({
|
||||
content: describeLightPolicy(homeAssistantService.getLightPolicyState?.() || {}),
|
||||
@@ -41,9 +69,35 @@ function createLightsCommand({ homeAssistantService, sanitizeMentions, config })
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'on' || action === 'off' || action === 'colors') {
|
||||
try {
|
||||
const result = action === 'colors'
|
||||
? await homeAssistantService.setRandomColorScene({ source: 'bot-command:lights:colors' })
|
||||
: await homeAssistantService.setAllControllableEntitiesState(action, {
|
||||
source: `bot-command:lights:${action}`,
|
||||
});
|
||||
const failed = result?.failures?.length || 0;
|
||||
const succeeded = result?.succeeded?.length || 0;
|
||||
const description = action === 'colors'
|
||||
? `Applied random colors to ${result?.colorLights || 0} RGB lights and requested off for ${result?.nonColorEntities || 0} non-RGB lights.`
|
||||
: `Turned ${action} ${succeeded} room lights.`;
|
||||
const failureSuffix = failed ? ` ${failed} failed.` : '';
|
||||
await message.reply({
|
||||
content: sanitizeMentions(`${description}${failureSuffix}`),
|
||||
allowedMentions: { parse: [], repliedUser: false },
|
||||
});
|
||||
} catch (err) {
|
||||
await message.reply({
|
||||
content: sanitizeMentions(`Failed to update room lights: ${err.message}`),
|
||||
allowedMentions: { parse: [], repliedUser: false },
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (action !== 'lock' && action !== 'unlock') {
|
||||
await message.reply({
|
||||
content: `Invalid lights command. Use \`${commandPrefix} lights lock\`, \`${commandPrefix} lights unlock\`, or \`${commandPrefix} lights status\`.`,
|
||||
content: `Invalid lights command. Use \`${commandPrefix} lights on\`, \`${commandPrefix} lights off\`, \`${commandPrefix} lights colors\`, \`${commandPrefix} lights lock\`, \`${commandPrefix} lights unlock\`, or \`${commandPrefix} lights status\`.`,
|
||||
allowedMentions: { parse: [], repliedUser: false },
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -93,9 +93,10 @@ function createCommandHandlers(deps) {
|
||||
return;
|
||||
}
|
||||
// Actions in this set can change operational safety or access policy, so
|
||||
// lockdown mode narrows them from normal admins to lockdown admins. Room
|
||||
// light locking belongs here because it can force the physical room lights
|
||||
// on and disables ordinary Home Assistant room controls for everyone else.
|
||||
// lockdown mode narrows them from normal admins to lockdown admins. Lights
|
||||
// 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', 'lights', 'kick', 'lift', 'neato']);
|
||||
const isAccessModeCommand = commandDefinition?.permission === 'access-mode';
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
// 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', 'lights', 'kick', 'verify', 'deter'] },
|
||||
features: { title: 'Features', names: ['lift', 'neato'] },
|
||||
admin: { title: 'Admin', names: ['lock', 'unlock', 'mode', 'reason', 'goal', 'kick', 'verify', 'deter'] },
|
||||
features: { title: 'Features', names: ['lights', 'lift', 'neato'] },
|
||||
discord: { title: 'Discord', names: ['bridge'] },
|
||||
};
|
||||
|
||||
@@ -19,7 +19,18 @@ 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' },
|
||||
lights: { category: 'admin', summary: 'Show or change the room-light lock.', usage: [`${prefix} lights <status|lock|unlock>`], access: 'Admin', permission: 'admin' },
|
||||
lights: {
|
||||
category: 'features',
|
||||
summary: 'Control room lights or manage the admin light lock.',
|
||||
usage: [
|
||||
`${prefix} lights <on|off|colors>`,
|
||||
`${prefix} lights <status|lock|unlock>`,
|
||||
],
|
||||
access: 'Light controls are public unless server access is restricted; lock controls require admin',
|
||||
permission: 'access-mode',
|
||||
requiredFeature: 'homeAssistant',
|
||||
unavailableLabel: 'Home Assistant',
|
||||
},
|
||||
kick: { category: 'admin', summary: 'Remove a user from their current rover.', usage: [`${prefix} kick <user> [reason]`], access: 'Admin', permission: 'admin' },
|
||||
verify: { category: 'admin', summary: 'List or remove verified identities.', usage: [`${prefix} verify list`, `${prefix} verify remove <identity>`], access: 'Lockdown admin', permission: 'lockdown-admin' },
|
||||
deter: { category: 'admin', summary: 'List, add, or remove identity deterrence.', usage: [`${prefix} deter list`, `${prefix} deter ban <identity>`, `${prefix} deter unban <identity>`], access: 'Lockdown admin', permission: 'lockdown-admin' },
|
||||
|
||||
@@ -61,6 +61,25 @@ function validateSources(list = [], socket = null) {
|
||||
}
|
||||
|
||||
function getDefaultWebSources(assignment = {}, socket = null) {
|
||||
/*
|
||||
PTZ ownership is intentionally tracked outside assignmentService because
|
||||
taking the camera releases the user's rover assignment. Check the PTZ
|
||||
service directly so a source-less web replay request, including `rs
|
||||
replay`, follows the camera currently controlled by that socket just as it
|
||||
follows an assigned rover below.
|
||||
|
||||
isOperator is deliberately stricter than PTZ access or queue membership:
|
||||
spectators and users waiting for a camera turn must not silently replay a
|
||||
camera they are not currently operating. Keeping this rule here also makes
|
||||
every web replay entry point share the same default instead of teaching the
|
||||
chat-command adapter about PTZ-specific state.
|
||||
*/
|
||||
if (ptzCameraService.getPublicState(socket).isOperator) {
|
||||
const source = ptzCameraService.getReplaySource();
|
||||
if (!source) return [];
|
||||
return [{ type: source.type, id: String(source.id), label: source.label || source.id }];
|
||||
}
|
||||
|
||||
if (assignment?.roverId) {
|
||||
const id = String(assignment.roverId);
|
||||
const match = getReplaySources(socket).find((entry) => entry.type === 'rover' && entry.id === id);
|
||||
|
||||
Reference in New Issue
Block a user