overseer light controls

This commit is contained in:
legop3
2026-06-06 17:23:57 -04:00
parent 889f660f3e
commit b21acc7f19
4 changed files with 112 additions and 3 deletions
@@ -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]));