mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
overseer light controls
This commit is contained in:
@@ -21,9 +21,25 @@ function toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster
|
|||||||
);
|
);
|
||||||
const entities = Array.isArray(homeAssistantState?.entities) ? homeAssistantState.entities : [];
|
const entities = Array.isArray(homeAssistantState?.entities) ? homeAssistantState.entities : [];
|
||||||
if (entities.length) {
|
if (entities.length) {
|
||||||
lines.push('home_assistant_entities:');
|
lines.push('home_assistant_room_lights:');
|
||||||
entities.slice(0, 24).forEach((entity) => {
|
entities.slice(0, 24).forEach((entity) => {
|
||||||
lines.push(`- ${entity.id} (${entity.type || 'entity'}) state=${entity.state || 'unknown'} available=${entity.available ? 'yes' : 'no'}`);
|
const haType = String(entity.type || 'entity');
|
||||||
|
const details = [
|
||||||
|
'kind=room_light',
|
||||||
|
`ha_domain=${haType}`,
|
||||||
|
`state=${entity.state || 'unknown'}`,
|
||||||
|
`available=${entity.available ? 'yes' : 'no'}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
// All configured Home Assistant controls in this list represent room
|
||||||
|
// lighting from the overseer's point of view, including outlet-backed
|
||||||
|
// lamps that Home Assistant exposes as switches. The original HA domain is
|
||||||
|
// still shown because only true light-domain entities can accept color
|
||||||
|
// payloads; switch-domain lamps remain valid on/off room lights.
|
||||||
|
details.push(`supports_color=${entity.supportsColor ? 'yes' : 'no'}`);
|
||||||
|
if (entity.colorHex) details.push(`color=${entity.colorHex}`);
|
||||||
|
|
||||||
|
lines.push(`- ${entity.id} ${details.join(' ')}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const roverLines = (Array.isArray(roster) ? roster : []).slice(0, 6).map((rover) => {
|
const roverLines = (Array.isArray(roster) ? roster : []).slice(0, 6).map((rover) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
id: 'ha_set_entity',
|
id: 'ha_set_entity',
|
||||||
signature: 'ha_set_entity(entity_id, state)',
|
signature: 'ha_set_entity(entity_id, state)',
|
||||||
description: 'Set Home Assistant controllable entity on/off.',
|
description: 'Turn a configured room light or Home Assistant controllable entity on/off, including switch-backed lamps.',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
@@ -25,6 +25,12 @@ module.exports = {
|
|||||||
async execute({ args = {}, homeAssistantService }) {
|
async execute({ args = {}, homeAssistantService }) {
|
||||||
const entityId = String(args?.entity_id || args?.entityId || '').trim();
|
const entityId = String(args?.entity_id || args?.entityId || '').trim();
|
||||||
if (!entityId) throw new Error('ha_set_entity requires args.entity_id');
|
if (!entityId) throw new Error('ha_set_entity requires args.entity_id');
|
||||||
|
|
||||||
|
// Configured Home Assistant entities are the room-control surface the
|
||||||
|
// overseer is allowed to use. Some physical room lights are exposed by Home
|
||||||
|
// Assistant as switches because they are outlet-backed lamps, so this tool
|
||||||
|
// intentionally permits every configured entity for on/off control instead
|
||||||
|
// of limiting itself to HA's light domain.
|
||||||
const allowed = new Set(
|
const allowed = new Set(
|
||||||
(homeAssistantService.getState()?.entities || []).map((entry) => String(entry?.id || '')).filter(Boolean),
|
(homeAssistantService.getState()?.entities || []).map((entry) => String(entry?.id || '')).filter(Boolean),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
function normalizeColorHex(value) {
|
||||||
|
const raw = String(value || '').trim();
|
||||||
|
const withoutHash = raw.startsWith('#') ? raw.slice(1) : raw;
|
||||||
|
|
||||||
|
// Overseer tool calls should be strict here because invalid color strings
|
||||||
|
// otherwise travel all the way to the Home Assistant runtime before failing.
|
||||||
|
// Keeping the accepted format to CSS-style hex also gives the model one clear
|
||||||
|
// representation to use instead of making it choose between RGB, HSL, names,
|
||||||
|
// or Home Assistant-specific payloads.
|
||||||
|
if (!/^[0-9a-fA-F]{3}$|^[0-9a-fA-F]{6}$/.test(withoutHash)) {
|
||||||
|
throw new Error('ha_set_light_color requires args.color_hex as #rgb or #rrggbb');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand shorthand colors before handing them to the shared HA service so
|
||||||
|
// logs, downstream payloads, and future tool results all use the same stable
|
||||||
|
// six-digit color shape.
|
||||||
|
const expanded =
|
||||||
|
withoutHash.length === 3
|
||||||
|
? withoutHash
|
||||||
|
.split('')
|
||||||
|
.map((char) => char + char)
|
||||||
|
.join('')
|
||||||
|
: withoutHash;
|
||||||
|
return `#${expanded.toLowerCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findConfiguredEntity(homeAssistantService, entityId) {
|
||||||
|
const entities = homeAssistantService.getState()?.entities || [];
|
||||||
|
|
||||||
|
// The overseer only gets to act on entities that the local config already
|
||||||
|
// exposes. This mirrors ha_set_entity and prevents a model-generated entity id
|
||||||
|
// from becoming an arbitrary Home Assistant service call.
|
||||||
|
return entities.find((entry) => String(entry?.id || '') === entityId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
id: 'ha_set_light_color',
|
||||||
|
signature: 'ha_set_light_color(entity_id, color_hex)',
|
||||||
|
description: 'Set a configured Home Assistant color-capable light to a hex color.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
entity_id: { type: 'string', minLength: 1 },
|
||||||
|
color_hex: {
|
||||||
|
type: 'string',
|
||||||
|
pattern: '^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$',
|
||||||
|
description: 'CSS-style RGB hex color such as #ff0000, #00ff88, or #f0a.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['entity_id', 'color_hex'],
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
availability(ctx = {}) {
|
||||||
|
const mode = String(ctx.mode || '');
|
||||||
|
if (mode === 'admin' || mode === 'lockdown') {
|
||||||
|
return { available: false, reason: `policy_lock:site_mode_${mode}` };
|
||||||
|
}
|
||||||
|
if (ctx.homeAssistantState?.lightPolicy?.lockedOn) {
|
||||||
|
return { available: false, reason: 'policy_lock:lights_locked_on' };
|
||||||
|
}
|
||||||
|
if (!ctx.homeAssistantState?.connected) return { available: false, reason: 'unavailable' };
|
||||||
|
return { available: true, reason: null };
|
||||||
|
},
|
||||||
|
async execute({ args = {}, homeAssistantService }) {
|
||||||
|
const entityId = String(args?.entity_id || args?.entityId || '').trim();
|
||||||
|
if (!entityId) throw new Error('ha_set_light_color requires args.entity_id');
|
||||||
|
|
||||||
|
// Accepting args.color as a compatibility alias keeps manual/internal calls
|
||||||
|
// forgiving, while the public tool schema still teaches the model to send
|
||||||
|
// the clearer color_hex argument.
|
||||||
|
const colorHex = normalizeColorHex(args?.color_hex ?? args?.colorHex ?? args?.color);
|
||||||
|
const entity = findConfiguredEntity(homeAssistantService, entityId);
|
||||||
|
if (!entity) throw new Error('ha_set_light_color entity_id not configured');
|
||||||
|
|
||||||
|
// setLightColor already requires a HA light, but checking the normalized
|
||||||
|
// entity state here gives the overseer a more specific error and blocks
|
||||||
|
// switch/outlet-backed lamps before they become invalid HA color commands.
|
||||||
|
if (entity.type !== 'light') throw new Error('ha_set_light_color requires a light entity');
|
||||||
|
if (entity.available === false) throw new Error('ha_set_light_color light entity unavailable');
|
||||||
|
if (!entity.supportsColor) throw new Error('ha_set_light_color light does not report color support');
|
||||||
|
|
||||||
|
await homeAssistantService.setLightColor(entityId, colorHex);
|
||||||
|
return { ok: true, entity_id: entityId, color_hex: colorHex };
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -10,6 +10,7 @@ const neatoSendHome = require('./neatoSendHome');
|
|||||||
// const neatoLocate = require('./neatoLocate');
|
// const neatoLocate = require('./neatoLocate');
|
||||||
const neatoClearErrors = require('./neatoClearErrors');
|
const neatoClearErrors = require('./neatoClearErrors');
|
||||||
const haSetEntity = require('./haSetEntity');
|
const haSetEntity = require('./haSetEntity');
|
||||||
|
const haSetLightColor = require('./haSetLightColor');
|
||||||
const buttonBoxAddCount = require('./buttonBoxAddCount');
|
const buttonBoxAddCount = require('./buttonBoxAddCount');
|
||||||
|
|
||||||
const TOOL_DEFINITIONS = [
|
const TOOL_DEFINITIONS = [
|
||||||
@@ -25,6 +26,7 @@ const TOOL_DEFINITIONS = [
|
|||||||
// neatoLocate,
|
// neatoLocate,
|
||||||
neatoClearErrors,
|
neatoClearErrors,
|
||||||
haSetEntity,
|
haSetEntity,
|
||||||
|
haSetLightColor,
|
||||||
buttonBoxAddCount,
|
buttonBoxAddCount,
|
||||||
];
|
];
|
||||||
const TOOL_BY_ID = new Map(TOOL_DEFINITIONS.map((tool) => [tool.id, tool]));
|
const TOOL_BY_ID = new Map(TOOL_DEFINITIONS.map((tool) => [tool.id, tool]));
|
||||||
|
|||||||
Reference in New Issue
Block a user