Restructure Project Architecture with Modular Routes and Configuration

- Refactored main application entry point to use centralized configuration
- Created modular route structure with separate files for different API endpoints
- Introduced app.config.ts for centralized environment variable management
- Moved tools and route logic into dedicated files
- Simplified index.ts and improved overall project organization
- Added comprehensive type definitions for tools and API interactions
This commit is contained in:
jango-blockchained
2025-02-03 15:39:19 +01:00
parent 18f09bb5ce
commit 397355c1ad
20 changed files with 1664 additions and 1341 deletions

106
src/tools/addon.tool.ts Normal file
View File

@@ -0,0 +1,106 @@
import { z } from 'zod';
import { Tool, AddonParams, HassAddonResponse, HassAddonInfoResponse } from '../types/index.js';
import { APP_CONFIG } from '../config/app.config.js';
export const addonTool: Tool = {
name: 'addon',
description: 'Manage Home Assistant add-ons',
parameters: z.object({
action: z.enum(['list', 'info', 'install', 'uninstall', 'start', 'stop', 'restart'])
.describe('Action to perform with add-on'),
slug: z.string().optional().describe('Add-on slug (required for all actions except list)'),
version: z.string().optional().describe('Version to install (only for install action)'),
}),
execute: async (params: AddonParams) => {
try {
if (params.action === 'list') {
const response = await fetch(`${APP_CONFIG.HASS_HOST}/api/hassio/store`, {
headers: {
Authorization: `Bearer ${APP_CONFIG.HASS_TOKEN}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch add-ons: ${response.statusText}`);
}
const data = await response.json() as HassAddonResponse;
return {
success: true,
addons: data.data.addons.map((addon) => ({
name: addon.name,
slug: addon.slug,
description: addon.description,
version: addon.version,
installed: addon.installed,
available: addon.available,
state: addon.state,
})),
};
} else {
if (!params.slug) {
throw new Error('Add-on slug is required for this action');
}
let endpoint = '';
let method = 'GET';
const body: Record<string, any> = {};
switch (params.action) {
case 'info':
endpoint = `/api/hassio/addons/${params.slug}/info`;
break;
case 'install':
endpoint = `/api/hassio/addons/${params.slug}/install`;
method = 'POST';
if (params.version) {
body.version = params.version;
}
break;
case 'uninstall':
endpoint = `/api/hassio/addons/${params.slug}/uninstall`;
method = 'POST';
break;
case 'start':
endpoint = `/api/hassio/addons/${params.slug}/start`;
method = 'POST';
break;
case 'stop':
endpoint = `/api/hassio/addons/${params.slug}/stop`;
method = 'POST';
break;
case 'restart':
endpoint = `/api/hassio/addons/${params.slug}/restart`;
method = 'POST';
break;
}
const response = await fetch(`${APP_CONFIG.HASS_HOST}${endpoint}`, {
method,
headers: {
Authorization: `Bearer ${APP_CONFIG.HASS_TOKEN}`,
'Content-Type': 'application/json',
},
...(Object.keys(body).length > 0 && { body: JSON.stringify(body) }),
});
if (!response.ok) {
throw new Error(`Failed to ${params.action} add-on: ${response.statusText}`);
}
const data = await response.json() as HassAddonInfoResponse;
return {
success: true,
message: `Successfully ${params.action}ed add-on ${params.slug}`,
data: data.data,
};
}
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
},
};

View File

@@ -0,0 +1,150 @@
import { z } from 'zod';
import { Tool, AutomationConfigParams, AutomationConfig, AutomationResponse } from '../types/index.js';
import { APP_CONFIG } from '../config/app.config.js';
export const automationConfigTool: Tool = {
name: 'automation_config',
description: 'Advanced automation configuration and management',
parameters: z.object({
action: z.enum(['create', 'update', 'delete', 'duplicate'])
.describe('Action to perform with automation config'),
automation_id: z.string().optional()
.describe('Automation ID (required for update, delete, and duplicate)'),
config: z.object({
alias: z.string().describe('Friendly name for the automation'),
description: z.string().optional().describe('Description of what the automation does'),
mode: z.enum(['single', 'parallel', 'queued', 'restart']).optional()
.describe('How multiple triggerings are handled'),
trigger: z.array(z.any()).describe('List of triggers'),
condition: z.array(z.any()).optional().describe('List of conditions'),
action: z.array(z.any()).describe('List of actions'),
}).optional().describe('Automation configuration (required for create and update)'),
}),
execute: async (params: AutomationConfigParams) => {
try {
switch (params.action) {
case 'create': {
if (!params.config) {
throw new Error('Configuration is required for creating automation');
}
const response = await fetch(`${APP_CONFIG.HASS_HOST}/api/config/automation/config`, {
method: 'POST',
headers: {
Authorization: `Bearer ${APP_CONFIG.HASS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(params.config),
});
if (!response.ok) {
throw new Error(`Failed to create automation: ${response.statusText}`);
}
const responseData = await response.json() as { automation_id: string };
return {
success: true,
message: 'Successfully created automation',
automation_id: responseData.automation_id,
};
}
case 'update': {
if (!params.automation_id || !params.config) {
throw new Error('Automation ID and configuration are required for updating automation');
}
const response = await fetch(`${APP_CONFIG.HASS_HOST}/api/config/automation/config/${params.automation_id}`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${APP_CONFIG.HASS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(params.config),
});
if (!response.ok) {
throw new Error(`Failed to update automation: ${response.statusText}`);
}
const responseData = await response.json() as { automation_id: string };
return {
success: true,
automation_id: responseData.automation_id,
message: 'Automation updated successfully'
};
}
case 'delete': {
if (!params.automation_id) {
throw new Error('Automation ID is required for deleting automation');
}
const response = await fetch(`${APP_CONFIG.HASS_HOST}/api/config/automation/config/${params.automation_id}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${APP_CONFIG.HASS_TOKEN}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to delete automation: ${response.statusText}`);
}
return {
success: true,
message: `Successfully deleted automation ${params.automation_id}`,
};
}
case 'duplicate': {
if (!params.automation_id) {
throw new Error('Automation ID is required for duplicating automation');
}
// First, get the existing automation config
const getResponse = await fetch(`${APP_CONFIG.HASS_HOST}/api/config/automation/config/${params.automation_id}`, {
headers: {
Authorization: `Bearer ${APP_CONFIG.HASS_TOKEN}`,
'Content-Type': 'application/json',
},
});
if (!getResponse.ok) {
throw new Error(`Failed to get automation config: ${getResponse.statusText}`);
}
const config = await getResponse.json() as AutomationConfig;
config.alias = `${config.alias} (Copy)`;
// Create new automation with modified config
const createResponse = await fetch(`${APP_CONFIG.HASS_HOST}/api/config/automation/config`, {
method: 'POST',
headers: {
Authorization: `Bearer ${APP_CONFIG.HASS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(config),
});
if (!createResponse.ok) {
throw new Error(`Failed to create duplicate automation: ${createResponse.statusText}`);
}
const newAutomation = await createResponse.json() as AutomationResponse;
return {
success: true,
message: `Successfully duplicated automation ${params.automation_id}`,
new_automation_id: newAutomation.automation_id,
};
}
}
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
},
};

View File

@@ -0,0 +1,73 @@
import { z } from 'zod';
import { Tool, AutomationParams, HassState, AutomationResponse } from '../types/index.js';
import { APP_CONFIG } from '../config/app.config.js';
export const automationTool: Tool = {
name: 'automation',
description: 'Manage Home Assistant automations',
parameters: z.object({
action: z.enum(['list', 'toggle', 'trigger']).describe('Action to perform with automation'),
automation_id: z.string().optional().describe('Automation ID (required for toggle and trigger actions)'),
}),
execute: async (params: AutomationParams) => {
try {
if (params.action === 'list') {
const response = await fetch(`${APP_CONFIG.HASS_HOST}/api/states`, {
headers: {
Authorization: `Bearer ${APP_CONFIG.HASS_TOKEN}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch automations: ${response.statusText}`);
}
const states = (await response.json()) as HassState[];
const automations = states.filter((state) => state.entity_id.startsWith('automation.'));
return {
success: true,
automations: automations.map((automation) => ({
entity_id: automation.entity_id,
name: automation.attributes.friendly_name || automation.entity_id.split('.')[1],
state: automation.state,
last_triggered: automation.attributes.last_triggered,
})),
};
} else {
if (!params.automation_id) {
throw new Error('Automation ID is required for toggle and trigger actions');
}
const service = params.action === 'toggle' ? 'toggle' : 'trigger';
const response = await fetch(`${APP_CONFIG.HASS_HOST}/api/services/automation/${service}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${APP_CONFIG.HASS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
entity_id: params.automation_id,
}),
});
if (!response.ok) {
throw new Error(`Failed to ${service} automation: ${response.statusText}`);
}
const responseData = await response.json() as AutomationResponse;
return {
success: true,
message: `Successfully ${service}d automation ${params.automation_id}`,
automation_id: responseData.automation_id,
};
}
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
},
};

139
src/tools/control.tool.ts Normal file
View File

@@ -0,0 +1,139 @@
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;
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;
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
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'
};
}
}
};

53
src/tools/history.tool.ts Normal file
View File

@@ -0,0 +1,53 @@
import { z } from 'zod';
import { Tool, HistoryParams } from '../types/index.js';
import { APP_CONFIG } from '../config/app.config.js';
export const historyTool: Tool = {
name: 'get_history',
description: 'Get state history for Home Assistant entities',
parameters: z.object({
entity_id: z.string().describe('The entity ID to get history for'),
start_time: z.string().optional().describe('Start time in ISO format. Defaults to 24 hours ago'),
end_time: z.string().optional().describe('End time in ISO format. Defaults to now'),
minimal_response: z.boolean().optional().describe('Return minimal response to reduce data size'),
significant_changes_only: z.boolean().optional().describe('Only return significant state changes'),
}),
execute: async (params: HistoryParams) => {
try {
const now = new Date();
const startTime = params.start_time ? new Date(params.start_time) : new Date(now.getTime() - 24 * 60 * 60 * 1000);
const endTime = params.end_time ? new Date(params.end_time) : now;
// Build query parameters
const queryParams = new URLSearchParams({
filter_entity_id: params.entity_id,
minimal_response: String(!!params.minimal_response),
significant_changes_only: String(!!params.significant_changes_only),
start_time: startTime.toISOString(),
end_time: endTime.toISOString(),
});
const response = await fetch(`${APP_CONFIG.HASS_HOST}/api/history/period/${startTime.toISOString()}?${queryParams.toString()}`, {
headers: {
Authorization: `Bearer ${APP_CONFIG.HASS_TOKEN}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch history: ${response.statusText}`);
}
const history = await response.json();
return {
success: true,
history,
};
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
},
};

View File

@@ -1,5 +1,17 @@
import { z } from 'zod';
import { get_hass } from '../hass/index.js';
import { Tool } from '../types/index.js';
import { listDevicesTool } from './list-devices.tool.js';
import { controlTool } from './control.tool.js';
import { historyTool } from './history.tool.js';
import { sceneTool } from './scene.tool.js';
import { notifyTool } from './notify.tool.js';
import { automationTool } from './automation.tool.js';
import { addonTool } from './addon.tool.js';
import { packageTool } from './package.tool.js';
import { automationConfigTool } from './automation-config.tool.js';
import { subscribeEventsTool } from './subscribe-events.tool.js';
import { getSSEStatsTool } from './sse-stats.tool.js';
// Tool category types
export enum ToolCategory {
@@ -188,4 +200,44 @@ export class LightControlTool implements EnhancedTool {
// Implementation here
return { success: true };
}
}
}
// Array to track all tools
const tools: Tool[] = [
listDevicesTool,
controlTool,
historyTool,
sceneTool,
notifyTool,
automationTool,
addonTool,
packageTool,
automationConfigTool,
subscribeEventsTool,
getSSEStatsTool
];
// Function to get a tool by name
export function getToolByName(name: string): Tool | undefined {
return tools.find(tool => tool.name === name);
}
// Function to get all tools
export function getAllTools(): Tool[] {
return [...tools];
}
// Export all tools individually
export {
listDevicesTool,
controlTool,
historyTool,
sceneTool,
notifyTool,
automationTool,
addonTool,
packageTool,
automationConfigTool,
subscribeEventsTool,
getSSEStatsTool
};

View File

@@ -0,0 +1,46 @@
import { z } from 'zod';
import { Tool } from '../types/index.js';
import { APP_CONFIG } from '../config/app.config.js';
import { HassState } from '../types/index.js';
export const listDevicesTool: Tool = {
name: 'list_devices',
description: 'List all available Home Assistant devices',
parameters: z.object({}).describe('No parameters required'),
execute: async () => {
try {
const response = await fetch(`${APP_CONFIG.HASS_HOST}/api/states`, {
headers: {
Authorization: `Bearer ${APP_CONFIG.HASS_TOKEN}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch devices: ${response.statusText}`);
}
const states = await response.json() as HassState[];
const devices: Record<string, HassState[]> = {};
// Group devices by domain
states.forEach(state => {
const [domain] = state.entity_id.split('.');
if (!devices[domain]) {
devices[domain] = [];
}
devices[domain].push(state);
});
return {
success: true,
devices
};
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred'
};
}
}
};

47
src/tools/notify.tool.ts Normal file
View File

@@ -0,0 +1,47 @@
import { z } from 'zod';
import { Tool, NotifyParams } from '../types/index.js';
import { APP_CONFIG } from '../config/app.config.js';
export const notifyTool: Tool = {
name: 'notify',
description: 'Send notifications through Home Assistant',
parameters: z.object({
message: z.string().describe('The notification message'),
title: z.string().optional().describe('The notification title'),
target: z.string().optional().describe('Specific notification target (e.g., mobile_app_phone)'),
data: z.record(z.any()).optional().describe('Additional notification data'),
}),
execute: async (params: NotifyParams) => {
try {
const service = params.target ? `notify.${params.target}` : 'notify.notify';
const [domain, service_name] = service.split('.');
const response = await fetch(`${APP_CONFIG.HASS_HOST}/api/services/${domain}/${service_name}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${APP_CONFIG.HASS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: params.message,
title: params.title,
data: params.data,
}),
});
if (!response.ok) {
throw new Error(`Failed to send notification: ${response.statusText}`);
}
return {
success: true,
message: 'Notification sent successfully',
};
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
},
};

88
src/tools/package.tool.ts Normal file
View File

@@ -0,0 +1,88 @@
import { z } from 'zod';
import { Tool, PackageParams, HacsResponse } from '../types/index.js';
import { APP_CONFIG } from '../config/app.config.js';
export const packageTool: Tool = {
name: 'package',
description: 'Manage HACS packages and custom components',
parameters: z.object({
action: z.enum(['list', 'install', 'uninstall', 'update'])
.describe('Action to perform with package'),
category: z.enum(['integration', 'plugin', 'theme', 'python_script', 'appdaemon', 'netdaemon'])
.describe('Package category'),
repository: z.string().optional().describe('Repository URL or name (required for install)'),
version: z.string().optional().describe('Version to install'),
}),
execute: async (params: PackageParams) => {
try {
const hacsBase = `${APP_CONFIG.HASS_HOST}/api/hacs`;
if (params.action === 'list') {
const response = await fetch(`${hacsBase}/repositories?category=${params.category}`, {
headers: {
Authorization: `Bearer ${APP_CONFIG.HASS_TOKEN}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch packages: ${response.statusText}`);
}
const data = await response.json() as HacsResponse;
return {
success: true,
packages: data.repositories,
};
} else {
if (!params.repository) {
throw new Error('Repository is required for this action');
}
let endpoint = '';
const body: Record<string, any> = {
category: params.category,
repository: params.repository,
};
switch (params.action) {
case 'install':
endpoint = '/repository/install';
if (params.version) {
body.version = params.version;
}
break;
case 'uninstall':
endpoint = '/repository/uninstall';
break;
case 'update':
endpoint = '/repository/update';
break;
}
const response = await fetch(`${hacsBase}${endpoint}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${APP_CONFIG.HASS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`Failed to ${params.action} package: ${response.statusText}`);
}
return {
success: true,
message: `Successfully ${params.action}ed package ${params.repository}`,
};
}
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
},
};

71
src/tools/scene.tool.ts Normal file
View File

@@ -0,0 +1,71 @@
import { z } from 'zod';
import { Tool, SceneParams, HassState } from '../types/index.js';
import { APP_CONFIG } from '../config/app.config.js';
export const sceneTool: Tool = {
name: 'scene',
description: 'Manage and activate Home Assistant scenes',
parameters: z.object({
action: z.enum(['list', 'activate']).describe('Action to perform with scenes'),
scene_id: z.string().optional().describe('Scene ID to activate (required for activate action)'),
}),
execute: async (params: SceneParams) => {
try {
if (params.action === 'list') {
const response = await fetch(`${APP_CONFIG.HASS_HOST}/api/states`, {
headers: {
Authorization: `Bearer ${APP_CONFIG.HASS_TOKEN}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Failed to fetch scenes: ${response.statusText}`);
}
const states = (await response.json()) as HassState[];
const scenes = states.filter((state) => state.entity_id.startsWith('scene.'));
return {
success: true,
scenes: scenes.map((scene) => ({
entity_id: scene.entity_id,
name: scene.attributes.friendly_name || scene.entity_id.split('.')[1],
description: scene.attributes.description,
})),
};
} else if (params.action === 'activate') {
if (!params.scene_id) {
throw new Error('Scene ID is required for activate action');
}
const response = await fetch(`${APP_CONFIG.HASS_HOST}/api/services/scene/turn_on`, {
method: 'POST',
headers: {
Authorization: `Bearer ${APP_CONFIG.HASS_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
entity_id: params.scene_id,
}),
});
if (!response.ok) {
throw new Error(`Failed to activate scene: ${response.statusText}`);
}
return {
success: true,
message: `Successfully activated scene ${params.scene_id}`,
};
}
throw new Error('Invalid action specified');
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
},
};

View File

@@ -0,0 +1,33 @@
import { z } from 'zod';
import { Tool } from '../types/index.js';
import { APP_CONFIG } from '../config/app.config.js';
import { sseManager } from '../sse/index.js';
export const getSSEStatsTool: Tool = {
name: 'get_sse_stats',
description: 'Get SSE connection statistics',
parameters: z.object({
token: z.string().describe('Authentication token (required)')
}),
execute: async (params: { token: string }) => {
try {
if (params.token !== APP_CONFIG.HASS_TOKEN) {
return {
success: false,
message: 'Authentication failed'
};
}
const stats = await sseManager.getStatistics();
return {
success: true,
statistics: stats
};
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error occurred'
};
}
}
};

View File

@@ -0,0 +1,84 @@
import { z } from 'zod';
import { v4 as uuidv4 } from 'uuid';
import { Tool, SSEParams } from '../types/index.js';
import { sseManager } from '../sse/index.js';
export const subscribeEventsTool: Tool = {
name: 'subscribe_events',
description: 'Subscribe to Home Assistant events via Server-Sent Events (SSE)',
parameters: z.object({
token: z.string().describe('Authentication token (required)'),
events: z.array(z.string()).optional().describe('List of event types to subscribe to'),
entity_id: z.string().optional().describe('Specific entity ID to monitor for state changes'),
domain: z.string().optional().describe('Domain to monitor (e.g., "light", "switch", etc.)'),
}),
execute: async (params: SSEParams) => {
const clientId = uuidv4();
// Set up SSE headers
const responseHeaders = {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
};
// Create SSE client
const client = {
id: clientId,
send: (data: string) => {
return {
headers: responseHeaders,
body: `data: ${data}\n\n`,
keepAlive: true
};
}
};
// Add client to SSE manager with authentication
const sseClient = sseManager.addClient(client, params.token);
if (!sseClient || !sseClient.authenticated) {
return {
success: false,
message: sseClient ? 'Authentication failed' : 'Maximum client limit reached'
};
}
// Subscribe to specific events if provided
if (params.events?.length) {
console.log(`Client ${clientId} subscribing to events:`, params.events);
for (const eventType of params.events) {
sseManager.subscribeToEvent(clientId, eventType);
}
}
// Subscribe to specific entity if provided
if (params.entity_id) {
console.log(`Client ${clientId} subscribing to entity:`, params.entity_id);
sseManager.subscribeToEntity(clientId, params.entity_id);
}
// Subscribe to domain if provided
if (params.domain) {
console.log(`Client ${clientId} subscribing to domain:`, params.domain);
sseManager.subscribeToDomain(clientId, params.domain);
}
return {
headers: responseHeaders,
body: `data: ${JSON.stringify({
type: 'connection',
status: 'connected',
id: clientId,
authenticated: true,
subscriptions: {
events: params.events || [],
entities: params.entity_id ? [params.entity_id] : [],
domains: params.domain ? [params.domain] : []
},
timestamp: new Date().toISOString()
})}\n\n`,
keepAlive: true
};
}
};