refactor: optimize configuration and tool implementations
- Standardized error handling across tool implementations - Improved return type consistency for tool execution results - Simplified configuration parsing and type definitions - Enhanced type safety for various configuration schemas - Cleaned up and normalized tool response structures - Updated SSE and event subscription tool implementations
This commit is contained in:
@@ -1,139 +1,191 @@
|
||||
import { z } from 'zod';
|
||||
import { Tool, CommandParams } from '../types/index.js';
|
||||
import { APP_CONFIG } from '../config/app.config.js';
|
||||
import { DomainSchema } from '../schemas.js';
|
||||
import { z } from "zod";
|
||||
import { Tool, CommandParams } from "../types/index.js";
|
||||
import { APP_CONFIG } from "../config/app.config.js";
|
||||
import { DomainSchema } from "../schemas.js";
|
||||
|
||||
// Define command constants
|
||||
const commonCommands = ['turn_on', 'turn_off', 'toggle'] as const;
|
||||
const coverCommands = [...commonCommands, 'open', 'close', 'stop', 'set_position', 'set_tilt_position'] as const;
|
||||
const climateCommands = [...commonCommands, 'set_temperature', 'set_hvac_mode', 'set_fan_mode', 'set_humidity'] as const;
|
||||
const commonCommands = ["turn_on", "turn_off", "toggle"] as const;
|
||||
const coverCommands = [
|
||||
...commonCommands,
|
||||
"open",
|
||||
"close",
|
||||
"stop",
|
||||
"set_position",
|
||||
"set_tilt_position",
|
||||
] as const;
|
||||
const climateCommands = [
|
||||
...commonCommands,
|
||||
"set_temperature",
|
||||
"set_hvac_mode",
|
||||
"set_fan_mode",
|
||||
"set_humidity",
|
||||
] as const;
|
||||
|
||||
export const controlTool: Tool = {
|
||||
name: 'control',
|
||||
description: 'Control Home Assistant devices and services',
|
||||
parameters: z.object({
|
||||
command: z.enum([...commonCommands, ...coverCommands, ...climateCommands])
|
||||
.describe('The command to execute'),
|
||||
entity_id: z.string().describe('The entity ID to control'),
|
||||
// Common parameters
|
||||
state: z.string().optional().describe('The desired state for the entity'),
|
||||
// Light parameters
|
||||
brightness: z.number().min(0).max(255).optional()
|
||||
.describe('Brightness level for lights (0-255)'),
|
||||
color_temp: z.number().optional()
|
||||
.describe('Color temperature for lights'),
|
||||
rgb_color: z.tuple([z.number(), z.number(), z.number()]).optional()
|
||||
.describe('RGB color values'),
|
||||
// Cover parameters
|
||||
position: z.number().min(0).max(100).optional()
|
||||
.describe('Position for covers (0-100)'),
|
||||
tilt_position: z.number().min(0).max(100).optional()
|
||||
.describe('Tilt position for covers (0-100)'),
|
||||
// Climate parameters
|
||||
temperature: z.number().optional()
|
||||
.describe('Target temperature for climate devices'),
|
||||
target_temp_high: z.number().optional()
|
||||
.describe('Target high temperature for climate devices'),
|
||||
target_temp_low: z.number().optional()
|
||||
.describe('Target low temperature for climate devices'),
|
||||
hvac_mode: z.enum(['off', 'heat', 'cool', 'heat_cool', 'auto', 'dry', 'fan_only']).optional()
|
||||
.describe('HVAC mode for climate devices'),
|
||||
fan_mode: z.enum(['auto', 'low', 'medium', 'high']).optional()
|
||||
.describe('Fan mode for climate devices'),
|
||||
humidity: z.number().min(0).max(100).optional()
|
||||
.describe('Target humidity for climate devices')
|
||||
}),
|
||||
execute: async (params: CommandParams) => {
|
||||
try {
|
||||
const domain = params.entity_id.split('.')[0] as keyof typeof DomainSchema.Values;
|
||||
name: "control",
|
||||
description: "Control Home Assistant devices and services",
|
||||
parameters: z.object({
|
||||
command: z
|
||||
.enum([...commonCommands, ...coverCommands, ...climateCommands])
|
||||
.describe("The command to execute"),
|
||||
entity_id: z.string().describe("The entity ID to control"),
|
||||
// Common parameters
|
||||
state: z.string().optional().describe("The desired state for the entity"),
|
||||
// Light parameters
|
||||
brightness: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(255)
|
||||
.optional()
|
||||
.describe("Brightness level for lights (0-255)"),
|
||||
color_temp: z.number().optional().describe("Color temperature for lights"),
|
||||
rgb_color: z
|
||||
.tuple([z.number(), z.number(), z.number()])
|
||||
.optional()
|
||||
.describe("RGB color values"),
|
||||
// Cover parameters
|
||||
position: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(100)
|
||||
.optional()
|
||||
.describe("Position for covers (0-100)"),
|
||||
tilt_position: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(100)
|
||||
.optional()
|
||||
.describe("Tilt position for covers (0-100)"),
|
||||
// Climate parameters
|
||||
temperature: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Target temperature for climate devices"),
|
||||
target_temp_high: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Target high temperature for climate devices"),
|
||||
target_temp_low: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("Target low temperature for climate devices"),
|
||||
hvac_mode: z
|
||||
.enum(["off", "heat", "cool", "heat_cool", "auto", "dry", "fan_only"])
|
||||
.optional()
|
||||
.describe("HVAC mode for climate devices"),
|
||||
fan_mode: z
|
||||
.enum(["auto", "low", "medium", "high"])
|
||||
.optional()
|
||||
.describe("Fan mode for climate devices"),
|
||||
humidity: z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(100)
|
||||
.optional()
|
||||
.describe("Target humidity for climate devices"),
|
||||
}),
|
||||
execute: async (params: CommandParams) => {
|
||||
try {
|
||||
const domain = params.entity_id.split(
|
||||
".",
|
||||
)[0] as keyof typeof DomainSchema.Values;
|
||||
|
||||
if (!Object.values(DomainSchema.Values).includes(domain)) {
|
||||
throw new Error(`Unsupported domain: ${domain}`);
|
||||
if (!Object.values(DomainSchema.Values).includes(domain)) {
|
||||
throw new Error(`Unsupported domain: ${domain}`);
|
||||
}
|
||||
|
||||
const service = params.command;
|
||||
const serviceData: Record<string, any> = {
|
||||
entity_id: params.entity_id,
|
||||
};
|
||||
|
||||
// Handle domain-specific parameters
|
||||
switch (domain) {
|
||||
case "light":
|
||||
if (params.brightness !== undefined) {
|
||||
serviceData.brightness = params.brightness;
|
||||
}
|
||||
if (params.color_temp !== undefined) {
|
||||
serviceData.color_temp = params.color_temp;
|
||||
}
|
||||
if (params.rgb_color !== undefined) {
|
||||
serviceData.rgb_color = params.rgb_color;
|
||||
}
|
||||
break;
|
||||
|
||||
case "cover":
|
||||
if (service === "set_position" && params.position !== undefined) {
|
||||
serviceData.position = params.position;
|
||||
}
|
||||
if (
|
||||
service === "set_tilt_position" &&
|
||||
params.tilt_position !== undefined
|
||||
) {
|
||||
serviceData.tilt_position = params.tilt_position;
|
||||
}
|
||||
break;
|
||||
|
||||
case "climate":
|
||||
if (service === "set_temperature") {
|
||||
if (params.temperature !== undefined) {
|
||||
serviceData.temperature = params.temperature;
|
||||
}
|
||||
|
||||
const service = params.command;
|
||||
const serviceData: Record<string, any> = {
|
||||
entity_id: params.entity_id
|
||||
};
|
||||
|
||||
// Handle domain-specific parameters
|
||||
switch (domain) {
|
||||
case 'light':
|
||||
if (params.brightness !== undefined) {
|
||||
serviceData.brightness = params.brightness;
|
||||
}
|
||||
if (params.color_temp !== undefined) {
|
||||
serviceData.color_temp = params.color_temp;
|
||||
}
|
||||
if (params.rgb_color !== undefined) {
|
||||
serviceData.rgb_color = params.rgb_color;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'cover':
|
||||
if (service === 'set_position' && params.position !== undefined) {
|
||||
serviceData.position = params.position;
|
||||
}
|
||||
if (service === 'set_tilt_position' && params.tilt_position !== undefined) {
|
||||
serviceData.tilt_position = params.tilt_position;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'climate':
|
||||
if (service === 'set_temperature') {
|
||||
if (params.temperature !== undefined) {
|
||||
serviceData.temperature = params.temperature;
|
||||
}
|
||||
if (params.target_temp_high !== undefined) {
|
||||
serviceData.target_temp_high = params.target_temp_high;
|
||||
}
|
||||
if (params.target_temp_low !== undefined) {
|
||||
serviceData.target_temp_low = params.target_temp_low;
|
||||
}
|
||||
}
|
||||
if (service === 'set_hvac_mode' && params.hvac_mode !== undefined) {
|
||||
serviceData.hvac_mode = params.hvac_mode;
|
||||
}
|
||||
if (service === 'set_fan_mode' && params.fan_mode !== undefined) {
|
||||
serviceData.fan_mode = params.fan_mode;
|
||||
}
|
||||
if (service === 'set_humidity' && params.humidity !== undefined) {
|
||||
serviceData.humidity = params.humidity;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'switch':
|
||||
case 'contact':
|
||||
// These domains only support basic operations (turn_on, turn_off, toggle)
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported operation for domain: ${domain}`);
|
||||
if (params.target_temp_high !== undefined) {
|
||||
serviceData.target_temp_high = params.target_temp_high;
|
||||
}
|
||||
|
||||
// Call Home Assistant service
|
||||
const response = await fetch(`${APP_CONFIG.HASS_HOST}/api/services/${domain}/${service}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${APP_CONFIG.HASS_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(serviceData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to execute ${service} for ${params.entity_id}: ${response.statusText}`);
|
||||
if (params.target_temp_low !== undefined) {
|
||||
serviceData.target_temp_low = params.target_temp_low;
|
||||
}
|
||||
}
|
||||
if (service === "set_hvac_mode" && params.hvac_mode !== undefined) {
|
||||
serviceData.hvac_mode = params.hvac_mode;
|
||||
}
|
||||
if (service === "set_fan_mode" && params.fan_mode !== undefined) {
|
||||
serviceData.fan_mode = params.fan_mode;
|
||||
}
|
||||
if (service === "set_humidity" && params.humidity !== undefined) {
|
||||
serviceData.humidity = params.humidity;
|
||||
}
|
||||
break;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully executed ${service} for ${params.entity_id}`
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
};
|
||||
}
|
||||
case "switch":
|
||||
case "contact":
|
||||
// These domains only support basic operations (turn_on, turn_off, toggle)
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported operation for domain: ${domain}`);
|
||||
}
|
||||
|
||||
// Call Home Assistant service
|
||||
const response = await fetch(
|
||||
`${APP_CONFIG.HASS_HOST}/api/services/${domain}/${service}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${APP_CONFIG.HASS_TOKEN}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(serviceData),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to execute ${service} for ${params.entity_id}: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully executed ${service} for ${params.entity_id}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error ? error.message : "Unknown error occurred",
|
||||
};
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user