mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21: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 : [];
|
||||
if (entities.length) {
|
||||
lines.push('home_assistant_entities:');
|
||||
lines.push('home_assistant_room_lights:');
|
||||
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) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
id: 'ha_set_entity',
|
||||
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: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -25,6 +25,12 @@ module.exports = {
|
||||
async execute({ args = {}, homeAssistantService }) {
|
||||
const entityId = String(args?.entity_id || args?.entityId || '').trim();
|
||||
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(
|
||||
(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 neatoClearErrors = require('./neatoClearErrors');
|
||||
const haSetEntity = require('./haSetEntity');
|
||||
const haSetLightColor = require('./haSetLightColor');
|
||||
const buttonBoxAddCount = require('./buttonBoxAddCount');
|
||||
|
||||
const TOOL_DEFINITIONS = [
|
||||
@@ -25,6 +26,7 @@ const TOOL_DEFINITIONS = [
|
||||
// neatoLocate,
|
||||
neatoClearErrors,
|
||||
haSetEntity,
|
||||
haSetLightColor,
|
||||
buttonBoxAddCount,
|
||||
];
|
||||
const TOOL_BY_ID = new Map(TOOL_DEFINITIONS.map((tool) => [tool.id, tool]));
|
||||
|
||||
Reference in New Issue
Block a user