Add Server-Sent Events (SSE) support for Home Assistant integration

- Implemented comprehensive SSE manager with advanced client management
- Added dynamic event subscription and broadcasting capabilities
- Created robust rate limiting and client connection tracking
- Enhanced Home Assistant event handling with new SSE endpoints
- Updated package.json with UUID dependency for client identification
- Expanded test coverage for SSE and WebSocket event handling
- Improved type definitions for Home Assistant events and entities
This commit is contained in:
jango-blockchained
2025-01-30 11:42:19 +01:00
parent 70c9287c68
commit 64e619252c
8 changed files with 872 additions and 196 deletions

View File

@@ -1,4 +1,19 @@
import './polyfills.js';
import { config } from 'dotenv';
import { resolve } from 'path';
import { v4 as uuidv4 } from 'uuid';
import { sseManager } from './sse/index.js';
// Load environment variables based on NODE_ENV
const envFile = process.env.NODE_ENV === 'production'
? '.env'
: process.env.NODE_ENV === 'test'
? '.env.test'
: '.env.development';
console.log(`Loading environment from ${envFile}`);
config({ path: resolve(process.cwd(), envFile) });
import { get_hass } from './hass/index.js';
import { LiteMCP } from 'litemcp';
import { z } from 'zod';
@@ -7,6 +22,9 @@ 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;
const PORT = process.env.PORT || 3000;
console.log('Initializing Home Assistant connection...');
interface CommandParams {
command: string;
@@ -113,6 +131,17 @@ interface AutomationResponse {
automation_id: string;
}
interface SSEHeaders {
onAbort?: () => void;
}
interface SSEParams {
token: string;
events?: string[];
entity_id?: string;
domain?: string;
}
async function main() {
const hass = await get_hass();
@@ -760,10 +789,11 @@ async function main() {
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: (await response.json()).automation_id,
automation_id: responseData.automation_id,
};
}
@@ -785,9 +815,11 @@ async function main() {
throw new Error(`Failed to update automation: ${response.statusText}`);
}
const responseData = await response.json() as { automation_id: string };
return {
success: true,
message: `Successfully updated automation ${params.automation_id}`,
automation_id: responseData.automation_id,
message: 'Automation updated successfully'
};
}
@@ -865,9 +897,119 @@ async function main() {
},
});
// Add SSE endpoint
server.addTool({
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
};
}
});
// Add statistics endpoint
server.addTool({
name: 'get_sse_stats',
description: 'Get SSE connection statistics',
parameters: z.object({
token: z.string().describe('Authentication token (required)')
}),
execute: async (params: { token: string }) => {
if (params.token !== HASS_TOKEN) {
return {
success: false,
message: 'Authentication failed'
};
}
return {
success: true,
statistics: sseManager.getStatistics()
};
}
});
console.log('Initializing MCP Server...');
// Start the server
await server.start();
console.log('MCP Server started');
console.log(`MCP Server started on port ${PORT}`);
console.log('Home Assistant server running on stdio');
console.log('SSE endpoints initialized');
// Log successful initialization
console.log('Server initialization complete. Ready to handle requests.');
}
main().catch(console.error);