- Introduced Dockerfile for building and running the application in a containerized environment. - Added .dockerignore to exclude unnecessary files from the Docker context. - Updated README.md with detailed Docker installation instructions and Node.js version management using nvm. - Refactored environment variable handling in src/index.ts and src/config/hass.config.ts for improved configuration management. - Enhanced TypeScript configuration to include JSON module resolution and updated exclusion patterns. - Updated .gitignore to include additional files for better environment management.
226 lines
7.7 KiB
TypeScript
226 lines
7.7 KiB
TypeScript
import { get_hass } from './hass/index.js';
|
|
import { LiteMCP } from 'litemcp';
|
|
import { z } from 'zod';
|
|
import { DomainSchema } from './schemas.js';
|
|
|
|
// Configuration
|
|
const HASS_HOST = process.env.HASS_HOST || 'http://192.168.178.63:8123';
|
|
const HASS_TOKEN = process.env.HASS_TOKEN;
|
|
|
|
interface CommandParams {
|
|
command: string;
|
|
entity_id: string;
|
|
// Common parameters
|
|
state?: string;
|
|
// Light parameters
|
|
brightness?: number;
|
|
color_temp?: number;
|
|
rgb_color?: [number, number, number];
|
|
// Cover parameters
|
|
position?: number;
|
|
tilt_position?: number;
|
|
// Climate parameters
|
|
temperature?: number;
|
|
target_temp_high?: number;
|
|
target_temp_low?: number;
|
|
hvac_mode?: string;
|
|
fan_mode?: string;
|
|
humidity?: number;
|
|
}
|
|
|
|
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;
|
|
|
|
async function main() {
|
|
const hass = await get_hass();
|
|
|
|
// Create MCP server
|
|
const server = new LiteMCP('home-assistant', '0.1.0');
|
|
|
|
// Add the list devices tool
|
|
server.addTool({
|
|
name: 'list_devices',
|
|
description: 'List all available Home Assistant devices',
|
|
parameters: z.object({}),
|
|
execute: async () => {
|
|
try {
|
|
const response = await fetch(`${HASS_HOST}/api/states`, {
|
|
headers: {
|
|
Authorization: `Bearer ${HASS_TOKEN}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch devices: ${response.statusText}`);
|
|
}
|
|
|
|
const states = await response.json();
|
|
const devices = states.reduce((acc: any, state: any) => {
|
|
const domain = state.entity_id.split('.')[0];
|
|
if (!acc[domain]) {
|
|
acc[domain] = [];
|
|
}
|
|
acc[domain].push({
|
|
entity_id: state.entity_id,
|
|
state: state.state,
|
|
attributes: state.attributes,
|
|
});
|
|
return acc;
|
|
}, {});
|
|
|
|
return {
|
|
success: true,
|
|
devices,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
message: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
};
|
|
}
|
|
},
|
|
});
|
|
|
|
// Add the Home Assistant control tool
|
|
server.addTool({
|
|
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}`);
|
|
}
|
|
|
|
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}`);
|
|
}
|
|
|
|
// Call Home Assistant service
|
|
try {
|
|
const response = await fetch(`${HASS_HOST}/api/services/${domain}/${service}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${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) {
|
|
throw new Error(`Failed to execute ${service} for ${params.entity_id}: ${error instanceof Error ? error.message : 'Unknown error occurred'}`);
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
message: error instanceof Error ? error.message : 'Unknown error occurred'
|
|
};
|
|
}
|
|
}
|
|
});
|
|
|
|
// Start the server
|
|
await server.start();
|
|
console.log('MCP Server started');
|
|
}
|
|
|
|
main().catch(console.error); |