Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eefbf790c3 | ||
|
|
942c175b90 | ||
|
|
10e895bb94 | ||
|
|
a1cc54f01f | ||
|
|
e3256682ba | ||
|
|
7635cce15a |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -87,4 +87,6 @@ site/
|
|||||||
# Python
|
# Python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
|
models/
|
||||||
10
README.md
10
README.md
@@ -58,17 +58,17 @@ Our architecture is engineered for performance, scalability, and security. The f
|
|||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
subgraph Client
|
subgraph Client
|
||||||
A[Client Application<br>(Web / Mobile / Voice)]
|
A["Client Application (Web/Mobile/Voice)"]
|
||||||
end
|
end
|
||||||
subgraph CDN
|
subgraph CDN
|
||||||
B[CDN / Cache]
|
B["CDN / Cache"]
|
||||||
end
|
end
|
||||||
subgraph Server
|
subgraph Server
|
||||||
C[Bun Native Server]
|
C["Bun Native Server"]
|
||||||
E[NLP Engine &<br>Language Processing Module]
|
E["NLP Engine & Language Processing Module"]
|
||||||
end
|
end
|
||||||
subgraph Integration
|
subgraph Integration
|
||||||
D[Home Assistant<br>(Devices, Lights, Thermostats)]
|
D["Home Assistant (Devices, Lights, Thermostats)"]
|
||||||
end
|
end
|
||||||
|
|
||||||
A -->|HTTP Request| B
|
A -->|HTTP Request| B
|
||||||
|
|||||||
77
__tests__/core/server.test.ts
Normal file
77
__tests__/core/server.test.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
|
||||||
|
import {
|
||||||
|
type MockLiteMCPInstance,
|
||||||
|
type Tool,
|
||||||
|
createMockLiteMCPInstance,
|
||||||
|
createMockServices,
|
||||||
|
setupTestEnvironment,
|
||||||
|
cleanupMocks
|
||||||
|
} from '../utils/test-utils';
|
||||||
|
|
||||||
|
describe('Home Assistant MCP Server', () => {
|
||||||
|
let liteMcpInstance: MockLiteMCPInstance;
|
||||||
|
let addToolCalls: Tool[];
|
||||||
|
let mocks: ReturnType<typeof setupTestEnvironment>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Setup test environment
|
||||||
|
mocks = setupTestEnvironment();
|
||||||
|
liteMcpInstance = createMockLiteMCPInstance();
|
||||||
|
|
||||||
|
// Import the module which will execute the main function
|
||||||
|
await import('../../src/index.js');
|
||||||
|
|
||||||
|
// Get the mock instance and tool calls
|
||||||
|
addToolCalls = liteMcpInstance.addTool.mock.calls.map(call => call.args[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupMocks({ liteMcpInstance, ...mocks });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should connect to Home Assistant', async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
// Verify connection
|
||||||
|
expect(mocks.mockFetch.mock.calls.length).toBeGreaterThan(0);
|
||||||
|
expect(liteMcpInstance.start.mock.calls.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle connection errors', async () => {
|
||||||
|
// Setup error response
|
||||||
|
mocks.mockFetch = mock(() => Promise.reject(new Error('Connection failed')));
|
||||||
|
globalThis.fetch = mocks.mockFetch;
|
||||||
|
|
||||||
|
// Import module again with error mock
|
||||||
|
await import('../../src/index.js');
|
||||||
|
|
||||||
|
// Verify error handling
|
||||||
|
expect(mocks.mockFetch.mock.calls.length).toBeGreaterThan(0);
|
||||||
|
expect(liteMcpInstance.start.mock.calls.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Tool Registration', () => {
|
||||||
|
test('should register all required tools', () => {
|
||||||
|
const toolNames = addToolCalls.map(tool => tool.name);
|
||||||
|
|
||||||
|
expect(toolNames).toContain('list_devices');
|
||||||
|
expect(toolNames).toContain('control');
|
||||||
|
expect(toolNames).toContain('get_history');
|
||||||
|
expect(toolNames).toContain('scene');
|
||||||
|
expect(toolNames).toContain('notify');
|
||||||
|
expect(toolNames).toContain('automation');
|
||||||
|
expect(toolNames).toContain('addon');
|
||||||
|
expect(toolNames).toContain('package');
|
||||||
|
expect(toolNames).toContain('automation_config');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should configure tools with correct parameters', () => {
|
||||||
|
const listDevicesTool = addToolCalls.find(tool => tool.name === 'list_devices');
|
||||||
|
expect(listDevicesTool).toBeDefined();
|
||||||
|
expect(listDevicesTool?.parameters).toBeDefined();
|
||||||
|
|
||||||
|
const controlTool = addToolCalls.find(tool => tool.name === 'control');
|
||||||
|
expect(controlTool).toBeDefined();
|
||||||
|
expect(controlTool?.parameters).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,15 +1,9 @@
|
|||||||
import { jest, describe, it, expect } from '@jest/globals';
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { formatToolCall } from "../src/utils/helpers";
|
||||||
// Helper function moved from src/helpers.ts
|
|
||||||
const formatToolCall = (obj: any, isError: boolean = false) => {
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: JSON.stringify(obj, null, 2), isError }],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('helpers', () => {
|
describe('helpers', () => {
|
||||||
describe('formatToolCall', () => {
|
describe('formatToolCall', () => {
|
||||||
it('should format an object into the correct structure', () => {
|
test('should format an object into the correct structure', () => {
|
||||||
const testObj = { name: 'test', value: 123 };
|
const testObj = { name: 'test', value: 123 };
|
||||||
const result = formatToolCall(testObj);
|
const result = formatToolCall(testObj);
|
||||||
|
|
||||||
@@ -22,7 +16,7 @@ describe('helpers', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle error cases correctly', () => {
|
test('should handle error cases correctly', () => {
|
||||||
const testObj = { error: 'test error' };
|
const testObj = { error: 'test error' };
|
||||||
const result = formatToolCall(testObj, true);
|
const result = formatToolCall(testObj, true);
|
||||||
|
|
||||||
@@ -35,7 +29,7 @@ describe('helpers', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle empty objects', () => {
|
test('should handle empty objects', () => {
|
||||||
const testObj = {};
|
const testObj = {};
|
||||||
const result = formatToolCall(testObj);
|
const result = formatToolCall(testObj);
|
||||||
|
|
||||||
@@ -47,5 +41,26 @@ describe('helpers', () => {
|
|||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should handle null and undefined', () => {
|
||||||
|
const nullResult = formatToolCall(null);
|
||||||
|
const undefinedResult = formatToolCall(undefined);
|
||||||
|
|
||||||
|
expect(nullResult).toEqual({
|
||||||
|
content: [{
|
||||||
|
type: 'text',
|
||||||
|
text: 'null',
|
||||||
|
isError: false
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(undefinedResult).toEqual({
|
||||||
|
content: [{
|
||||||
|
type: 'text',
|
||||||
|
text: 'undefined',
|
||||||
|
isError: false
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,42 +1,15 @@
|
|||||||
import { jest, describe, beforeEach, afterEach, it, expect } from '@jest/globals';
|
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
|
||||||
import { LiteMCP } from 'litemcp';
|
import type { Mock } from "bun:test";
|
||||||
import { get_hass } from '../src/hass/index.js';
|
|
||||||
import type { WebSocket } from 'ws';
|
import type { WebSocket } from 'ws';
|
||||||
|
import type { LiteMCP } from 'litemcp';
|
||||||
|
|
||||||
// Load test environment variables with defaults
|
// Extend the global scope
|
||||||
const TEST_HASS_HOST = process.env.TEST_HASS_HOST || 'http://localhost:8123';
|
declare global {
|
||||||
const TEST_HASS_TOKEN = process.env.TEST_HASS_TOKEN || 'test_token';
|
// eslint-disable-next-line no-var
|
||||||
const TEST_HASS_SOCKET_URL = process.env.TEST_HASS_SOCKET_URL || 'ws://localhost:8123/api/websocket';
|
var mockResponse: Response;
|
||||||
|
}
|
||||||
|
|
||||||
// Set environment variables for testing
|
// Types
|
||||||
process.env.HASS_HOST = TEST_HASS_HOST;
|
|
||||||
process.env.HASS_TOKEN = TEST_HASS_TOKEN;
|
|
||||||
process.env.HASS_SOCKET_URL = TEST_HASS_SOCKET_URL;
|
|
||||||
|
|
||||||
// Mock fetch
|
|
||||||
const mockFetchResponse = {
|
|
||||||
ok: true,
|
|
||||||
status: 200,
|
|
||||||
statusText: 'OK',
|
|
||||||
json: async () => ({ automation_id: 'test_automation' }),
|
|
||||||
text: async () => '{"automation_id":"test_automation"}',
|
|
||||||
headers: new Headers(),
|
|
||||||
body: null,
|
|
||||||
bodyUsed: false,
|
|
||||||
arrayBuffer: async () => new ArrayBuffer(0),
|
|
||||||
blob: async () => new Blob([]),
|
|
||||||
formData: async () => new FormData(),
|
|
||||||
clone: function () { return { ...this }; },
|
|
||||||
type: 'default',
|
|
||||||
url: '',
|
|
||||||
redirected: false,
|
|
||||||
redirect: () => Promise.resolve(new Response())
|
|
||||||
} as Response;
|
|
||||||
|
|
||||||
const mockFetch = jest.fn(async (_input: string | URL | Request, _init?: RequestInit) => mockFetchResponse);
|
|
||||||
(global as any).fetch = mockFetch;
|
|
||||||
|
|
||||||
// Mock LiteMCP
|
|
||||||
interface Tool {
|
interface Tool {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -44,30 +17,18 @@ interface Tool {
|
|||||||
execute: (params: Record<string, unknown>) => Promise<unknown>;
|
execute: (params: Record<string, unknown>) => Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type MockFunction<T = any> = jest.Mock<Promise<T>, any[]>;
|
|
||||||
|
|
||||||
interface MockLiteMCPInstance {
|
interface MockLiteMCPInstance {
|
||||||
addTool: ReturnType<typeof jest.fn>;
|
addTool: Mock<(tool: Tool) => void>;
|
||||||
start: ReturnType<typeof jest.fn>;
|
start: Mock<() => Promise<void>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockLiteMCPInstance: MockLiteMCPInstance = {
|
|
||||||
addTool: jest.fn(),
|
|
||||||
start: jest.fn().mockResolvedValue(undefined)
|
|
||||||
};
|
|
||||||
|
|
||||||
jest.mock('litemcp', () => ({
|
|
||||||
LiteMCP: jest.fn(() => mockLiteMCPInstance)
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock get_hass
|
|
||||||
interface MockServices {
|
interface MockServices {
|
||||||
light: {
|
light: {
|
||||||
turn_on: jest.Mock;
|
turn_on: Mock<() => Promise<{ success: boolean }>>;
|
||||||
turn_off: jest.Mock;
|
turn_off: Mock<() => Promise<{ success: boolean }>>;
|
||||||
};
|
};
|
||||||
climate: {
|
climate: {
|
||||||
set_temperature: jest.Mock;
|
set_temperature: Mock<() => Promise<{ success: boolean }>>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,21 +36,6 @@ interface MockHassInstance {
|
|||||||
services: MockServices;
|
services: MockServices;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create mock services
|
|
||||||
const mockServices: MockServices = {
|
|
||||||
light: {
|
|
||||||
turn_on: jest.fn().mockResolvedValue({ success: true }),
|
|
||||||
turn_off: jest.fn().mockResolvedValue({ success: true })
|
|
||||||
},
|
|
||||||
climate: {
|
|
||||||
set_temperature: jest.fn().mockResolvedValue({ success: true })
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
jest.unstable_mockModule('../src/hass/index.js', () => ({
|
|
||||||
get_hass: jest.fn().mockResolvedValue({ services: mockServices })
|
|
||||||
}));
|
|
||||||
|
|
||||||
interface TestResponse {
|
interface TestResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
@@ -103,99 +49,212 @@ interface TestResponse {
|
|||||||
new_automation_id?: string;
|
new_automation_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebSocketEventMap = {
|
// Test configuration
|
||||||
message: MessageEvent;
|
const TEST_CONFIG = {
|
||||||
open: Event;
|
HASS_HOST: process.env.TEST_HASS_HOST || 'http://localhost:8123',
|
||||||
close: Event;
|
HASS_TOKEN: process.env.TEST_HASS_TOKEN || 'test_token',
|
||||||
error: Event;
|
HASS_SOCKET_URL: process.env.TEST_HASS_SOCKET_URL || 'ws://localhost:8123/api/websocket'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Setup test environment
|
||||||
|
Object.entries(TEST_CONFIG).forEach(([key, value]) => {
|
||||||
|
process.env[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock instances
|
||||||
|
const mockLiteMCPInstance: MockLiteMCPInstance = {
|
||||||
|
addTool: mock((tool: Tool) => undefined),
|
||||||
|
start: mock(() => Promise.resolve())
|
||||||
};
|
};
|
||||||
|
|
||||||
type WebSocketEventListener = (event: Event) => void;
|
const mockServices: MockServices = {
|
||||||
type WebSocketMessageListener = (event: MessageEvent) => void;
|
light: {
|
||||||
|
turn_on: mock(() => Promise.resolve({ success: true })),
|
||||||
|
turn_off: mock(() => Promise.resolve({ success: true }))
|
||||||
|
},
|
||||||
|
climate: {
|
||||||
|
set_temperature: mock(() => Promise.resolve({ success: true }))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
interface MockWebSocketInstance {
|
// Mock WebSocket
|
||||||
addEventListener: jest.Mock;
|
class MockWebSocket implements Partial<WebSocket> {
|
||||||
removeEventListener: jest.Mock;
|
public static readonly CONNECTING = 0;
|
||||||
send: jest.Mock;
|
public static readonly OPEN = 1;
|
||||||
close: jest.Mock;
|
public static readonly CLOSING = 2;
|
||||||
readyState: number;
|
public static readonly CLOSED = 3;
|
||||||
binaryType: 'blob' | 'arraybuffer';
|
|
||||||
bufferedAmount: number;
|
public readyState: 0 | 1 | 2 | 3 = MockWebSocket.OPEN;
|
||||||
extensions: string;
|
public bufferedAmount = 0;
|
||||||
protocol: string;
|
public extensions = '';
|
||||||
url: string;
|
public protocol = '';
|
||||||
onopen: WebSocketEventListener | null;
|
public url = '';
|
||||||
onerror: WebSocketEventListener | null;
|
public binaryType: 'arraybuffer' | 'nodebuffer' | 'fragments' = 'arraybuffer';
|
||||||
onclose: WebSocketEventListener | null;
|
|
||||||
onmessage: WebSocketMessageListener | null;
|
public onopen: ((event: any) => void) | null = null;
|
||||||
CONNECTING: number;
|
public onerror: ((event: any) => void) | null = null;
|
||||||
OPEN: number;
|
public onclose: ((event: any) => void) | null = null;
|
||||||
CLOSING: number;
|
public onmessage: ((event: any) => void) | null = null;
|
||||||
CLOSED: number;
|
|
||||||
|
public addEventListener = mock(() => undefined);
|
||||||
|
public removeEventListener = mock(() => undefined);
|
||||||
|
public send = mock(() => undefined);
|
||||||
|
public close = mock(() => undefined);
|
||||||
|
public ping = mock(() => undefined);
|
||||||
|
public pong = mock(() => undefined);
|
||||||
|
public terminate = mock(() => undefined);
|
||||||
|
public dispatchEvent = mock(() => true);
|
||||||
|
|
||||||
|
constructor(url: string | URL, protocols?: string | string[]) {
|
||||||
|
this.url = url.toString();
|
||||||
|
if (protocols) {
|
||||||
|
this.protocol = Array.isArray(protocols) ? protocols[0] : protocols;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createMockWebSocket = (): MockWebSocketInstance => ({
|
// Create fetch mock with implementation
|
||||||
addEventListener: jest.fn(),
|
let mockFetch = mock(() => {
|
||||||
removeEventListener: jest.fn(),
|
return Promise.resolve(new Response());
|
||||||
send: jest.fn(),
|
|
||||||
close: jest.fn(),
|
|
||||||
readyState: 0,
|
|
||||||
binaryType: 'blob',
|
|
||||||
bufferedAmount: 0,
|
|
||||||
extensions: '',
|
|
||||||
protocol: '',
|
|
||||||
url: '',
|
|
||||||
onopen: null,
|
|
||||||
onerror: null,
|
|
||||||
onclose: null,
|
|
||||||
onmessage: null,
|
|
||||||
CONNECTING: 0,
|
|
||||||
OPEN: 1,
|
|
||||||
CLOSING: 2,
|
|
||||||
CLOSED: 3
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Override globals
|
||||||
|
globalThis.fetch = mockFetch;
|
||||||
|
// Use type assertion to handle WebSocket compatibility
|
||||||
|
globalThis.WebSocket = MockWebSocket as any;
|
||||||
|
|
||||||
describe('Home Assistant MCP Server', () => {
|
describe('Home Assistant MCP Server', () => {
|
||||||
let mockHass: MockHassInstance;
|
let mockHass: MockHassInstance;
|
||||||
let liteMcpInstance: MockLiteMCPInstance;
|
let liteMcpInstance: MockLiteMCPInstance;
|
||||||
let addToolCalls: Array<[Tool]>;
|
let addToolCalls: Tool[];
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockHass = {
|
mockHass = {
|
||||||
services: mockServices
|
services: mockServices
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reset all mocks
|
// Reset mocks
|
||||||
jest.clearAllMocks();
|
mockLiteMCPInstance.addTool.mock.calls = [];
|
||||||
mockFetch.mockClear();
|
mockLiteMCPInstance.start.mock.calls = [];
|
||||||
|
|
||||||
|
// Setup default response
|
||||||
|
mockFetch = mock(() => {
|
||||||
|
return Promise.resolve(new Response(
|
||||||
|
JSON.stringify({ state: 'connected' }),
|
||||||
|
{ status: 200 }
|
||||||
|
));
|
||||||
|
});
|
||||||
|
globalThis.fetch = mockFetch;
|
||||||
|
|
||||||
// Import the module which will execute the main function
|
// Import the module which will execute the main function
|
||||||
await import('../src/index.js');
|
await import('../src/index.js');
|
||||||
|
|
||||||
// Mock WebSocket
|
|
||||||
const mockWs = createMockWebSocket();
|
|
||||||
(global as any).WebSocket = jest.fn(() => mockWs);
|
|
||||||
|
|
||||||
// Get the mock instance
|
// Get the mock instance
|
||||||
liteMcpInstance = mockLiteMCPInstance;
|
liteMcpInstance = mockLiteMCPInstance;
|
||||||
addToolCalls = liteMcpInstance.addTool.mock.calls as Array<[Tool]>;
|
addToolCalls = mockLiteMCPInstance.addTool.mock.calls.map(call => call.args[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.resetModules();
|
// Clean up
|
||||||
|
mockLiteMCPInstance.addTool.mock.calls = [];
|
||||||
|
mockLiteMCPInstance.start.mock.calls = [];
|
||||||
|
mockFetch = mock(() => Promise.resolve(new Response()));
|
||||||
|
globalThis.fetch = mockFetch;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should connect to Home Assistant', async () => {
|
test('should connect to Home Assistant', async () => {
|
||||||
const hass = await get_hass();
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
expect(hass).toBeDefined();
|
// Verify connection
|
||||||
expect(hass.services).toBeDefined();
|
expect(mockFetch.mock.calls.length).toBeGreaterThan(0);
|
||||||
expect(typeof hass.services.light.turn_on).toBe('function');
|
expect(mockLiteMCPInstance.start.mock.calls.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reuse the same instance on subsequent calls', async () => {
|
test('should handle connection errors', async () => {
|
||||||
const firstInstance = await get_hass();
|
// Setup error response
|
||||||
const secondInstance = await get_hass();
|
mockFetch = mock(() => Promise.reject(new Error('Connection failed')));
|
||||||
expect(firstInstance).toBe(secondInstance);
|
globalThis.fetch = mockFetch;
|
||||||
|
|
||||||
|
// Import module again with error mock
|
||||||
|
await import('../src/index.js');
|
||||||
|
|
||||||
|
// Verify error handling
|
||||||
|
expect(mockFetch.mock.calls.length).toBeGreaterThan(0);
|
||||||
|
expect(mockLiteMCPInstance.start.mock.calls.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Tool Registration', () => {
|
||||||
|
test('should register all required tools', () => {
|
||||||
|
const toolNames = addToolCalls.map(tool => tool.name);
|
||||||
|
|
||||||
|
expect(toolNames).toContain('list_devices');
|
||||||
|
expect(toolNames).toContain('control');
|
||||||
|
expect(toolNames).toContain('get_history');
|
||||||
|
expect(toolNames).toContain('scene');
|
||||||
|
expect(toolNames).toContain('notify');
|
||||||
|
expect(toolNames).toContain('automation');
|
||||||
|
expect(toolNames).toContain('addon');
|
||||||
|
expect(toolNames).toContain('package');
|
||||||
|
expect(toolNames).toContain('automation_config');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should configure tools with correct parameters', () => {
|
||||||
|
const listDevicesTool = addToolCalls.find(tool => tool.name === 'list_devices');
|
||||||
|
expect(listDevicesTool).toBeDefined();
|
||||||
|
expect(listDevicesTool?.parameters).toBeDefined();
|
||||||
|
|
||||||
|
const controlTool = addToolCalls.find(tool => tool.name === 'control');
|
||||||
|
expect(controlTool).toBeDefined();
|
||||||
|
expect(controlTool?.parameters).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Tool Execution', () => {
|
||||||
|
test('should execute list_devices tool', async () => {
|
||||||
|
const listDevicesTool = addToolCalls.find(tool => tool.name === 'list_devices');
|
||||||
|
expect(listDevicesTool).toBeDefined();
|
||||||
|
|
||||||
|
if (listDevicesTool) {
|
||||||
|
const mockDevices = [
|
||||||
|
{
|
||||||
|
entity_id: 'light.living_room',
|
||||||
|
state: 'on',
|
||||||
|
attributes: { brightness: 255 }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Setup response for this test
|
||||||
|
mockFetch = mock(() => Promise.resolve(new Response(
|
||||||
|
JSON.stringify(mockDevices)
|
||||||
|
)));
|
||||||
|
globalThis.fetch = mockFetch;
|
||||||
|
|
||||||
|
const result = await listDevicesTool.execute({}) as TestResponse;
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.devices).toBeDefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should execute control tool', async () => {
|
||||||
|
const controlTool = addToolCalls.find(tool => tool.name === 'control');
|
||||||
|
expect(controlTool).toBeDefined();
|
||||||
|
|
||||||
|
if (controlTool) {
|
||||||
|
// Setup response for this test
|
||||||
|
mockFetch = mock(() => Promise.resolve(new Response(
|
||||||
|
JSON.stringify({ success: true })
|
||||||
|
)));
|
||||||
|
globalThis.fetch = mockFetch;
|
||||||
|
|
||||||
|
const result = await controlTool.execute({
|
||||||
|
command: 'turn_on',
|
||||||
|
entity_id: 'light.living_room',
|
||||||
|
brightness: 255
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(mockFetch.mock.calls.length).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('list_devices tool', () => {
|
describe('list_devices tool', () => {
|
||||||
@@ -220,7 +279,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
} as Response);
|
} as Response);
|
||||||
|
|
||||||
// Get the tool registration
|
// Get the tool registration
|
||||||
const listDevicesTool = addToolCalls.find(call => call[0].name === 'list_devices')?.[0];
|
const listDevicesTool = addToolCalls.find(call => call.name === 'list_devices');
|
||||||
expect(listDevicesTool).toBeDefined();
|
expect(listDevicesTool).toBeDefined();
|
||||||
|
|
||||||
if (!listDevicesTool) {
|
if (!listDevicesTool) {
|
||||||
@@ -251,7 +310,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
mockFetch.mockRejectedValueOnce(new Error('Network error'));
|
mockFetch.mockRejectedValueOnce(new Error('Network error'));
|
||||||
|
|
||||||
// Get the tool registration
|
// Get the tool registration
|
||||||
const listDevicesTool = addToolCalls.find(call => call[0].name === 'list_devices')?.[0];
|
const listDevicesTool = addToolCalls.find(call => call.name === 'list_devices');
|
||||||
expect(listDevicesTool).toBeDefined();
|
expect(listDevicesTool).toBeDefined();
|
||||||
|
|
||||||
if (!listDevicesTool) {
|
if (!listDevicesTool) {
|
||||||
@@ -276,7 +335,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
} as Response);
|
} as Response);
|
||||||
|
|
||||||
// Get the tool registration
|
// Get the tool registration
|
||||||
const controlTool = addToolCalls.find(call => call[0].name === 'control')?.[0];
|
const controlTool = addToolCalls.find(call => call.name === 'control');
|
||||||
expect(controlTool).toBeDefined();
|
expect(controlTool).toBeDefined();
|
||||||
|
|
||||||
if (!controlTool) {
|
if (!controlTool) {
|
||||||
@@ -296,11 +355,11 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
|
|
||||||
// Verify the fetch call
|
// Verify the fetch call
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
`${TEST_HASS_HOST}/api/services/light/turn_on`,
|
`${TEST_CONFIG.HASS_HOST}/api/services/light/turn_on`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -313,7 +372,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
|
|
||||||
it('should handle unsupported domains', async () => {
|
it('should handle unsupported domains', async () => {
|
||||||
// Get the tool registration
|
// Get the tool registration
|
||||||
const controlTool = addToolCalls.find(call => call[0].name === 'control')?.[0];
|
const controlTool = addToolCalls.find(call => call.name === 'control');
|
||||||
expect(controlTool).toBeDefined();
|
expect(controlTool).toBeDefined();
|
||||||
|
|
||||||
if (!controlTool) {
|
if (!controlTool) {
|
||||||
@@ -339,7 +398,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
} as Response);
|
} as Response);
|
||||||
|
|
||||||
// Get the tool registration
|
// Get the tool registration
|
||||||
const controlTool = addToolCalls.find(call => call[0].name === 'control')?.[0];
|
const controlTool = addToolCalls.find(call => call.name === 'control');
|
||||||
expect(controlTool).toBeDefined();
|
expect(controlTool).toBeDefined();
|
||||||
|
|
||||||
if (!controlTool) {
|
if (!controlTool) {
|
||||||
@@ -365,7 +424,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
} as Response);
|
} as Response);
|
||||||
|
|
||||||
// Get the tool registration
|
// Get the tool registration
|
||||||
const controlTool = addToolCalls.find(call => call[0].name === 'control')?.[0];
|
const controlTool = addToolCalls.find(call => call.name === 'control');
|
||||||
expect(controlTool).toBeDefined();
|
expect(controlTool).toBeDefined();
|
||||||
|
|
||||||
if (!controlTool) {
|
if (!controlTool) {
|
||||||
@@ -387,11 +446,11 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
|
|
||||||
// Verify the fetch call
|
// Verify the fetch call
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
`${TEST_HASS_HOST}/api/services/climate/set_temperature`,
|
`${TEST_CONFIG.HASS_HOST}/api/services/climate/set_temperature`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -412,7 +471,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
} as Response);
|
} as Response);
|
||||||
|
|
||||||
// Get the tool registration
|
// Get the tool registration
|
||||||
const controlTool = addToolCalls.find(call => call[0].name === 'control')?.[0];
|
const controlTool = addToolCalls.find(call => call.name === 'control');
|
||||||
expect(controlTool).toBeDefined();
|
expect(controlTool).toBeDefined();
|
||||||
|
|
||||||
if (!controlTool) {
|
if (!controlTool) {
|
||||||
@@ -432,11 +491,11 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
|
|
||||||
// Verify the fetch call
|
// Verify the fetch call
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
`${TEST_HASS_HOST}/api/services/cover/set_position`,
|
`${TEST_CONFIG.HASS_HOST}/api/services/cover/set_position`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -449,36 +508,29 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('get_history tool', () => {
|
describe('get_history tool', () => {
|
||||||
it('should successfully fetch history', async () => {
|
test('should successfully fetch history', async () => {
|
||||||
const mockHistory = [
|
const mockHistory = [
|
||||||
{
|
{
|
||||||
entity_id: 'light.living_room',
|
entity_id: 'light.living_room',
|
||||||
state: 'on',
|
state: 'on',
|
||||||
last_changed: '2024-01-01T00:00:00Z',
|
last_changed: '2024-01-01T00:00:00Z',
|
||||||
attributes: { brightness: 255 }
|
attributes: { brightness: 255 }
|
||||||
},
|
|
||||||
{
|
|
||||||
entity_id: 'light.living_room',
|
|
||||||
state: 'off',
|
|
||||||
last_changed: '2024-01-01T01:00:00Z',
|
|
||||||
attributes: { brightness: 0 }
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
mockFetch.mockResolvedValueOnce({
|
// Setup response for this test
|
||||||
ok: true,
|
mockFetch = mock(() => Promise.resolve(new Response(
|
||||||
json: async () => mockHistory
|
JSON.stringify(mockHistory)
|
||||||
} as Response);
|
)));
|
||||||
|
globalThis.fetch = mockFetch;
|
||||||
|
|
||||||
// Get the tool registration
|
const historyTool = addToolCalls.find(call => call.name === 'get_history');
|
||||||
const historyTool = addToolCalls.find(call => call[0].name === 'get_history')?.[0];
|
|
||||||
expect(historyTool).toBeDefined();
|
expect(historyTool).toBeDefined();
|
||||||
|
|
||||||
if (!historyTool) {
|
if (!historyTool) {
|
||||||
throw new Error('get_history tool not found');
|
throw new Error('get_history tool not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the tool
|
|
||||||
const result = (await historyTool.execute({
|
const result = (await historyTool.execute({
|
||||||
entity_id: 'light.living_room',
|
entity_id: 'light.living_room',
|
||||||
start_time: '2024-01-01T00:00:00Z',
|
start_time: '2024-01-01T00:00:00Z',
|
||||||
@@ -491,29 +543,36 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.history).toEqual(mockHistory);
|
expect(result.history).toEqual(mockHistory);
|
||||||
|
|
||||||
// Verify the fetch call
|
// Verify the fetch call was made with correct URL and parameters
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
const calls = mockFetch.mock.calls;
|
||||||
expect.stringContaining('/api/history/period/2024-01-01T00:00:00Z?'),
|
expect(calls.length).toBeGreaterThan(0);
|
||||||
expect.objectContaining({
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Verify query parameters
|
const firstCall = calls[0];
|
||||||
const url = mockFetch.mock.calls[0][0] as string;
|
if (!firstCall?.args) {
|
||||||
const queryParams = new URL(url).searchParams;
|
throw new Error('No fetch calls recorded');
|
||||||
expect(queryParams.get('filter_entity_id')).toBe('light.living_room');
|
}
|
||||||
expect(queryParams.get('minimal_response')).toBe('true');
|
|
||||||
expect(queryParams.get('significant_changes_only')).toBe('true');
|
const [urlStr, options] = firstCall.args;
|
||||||
|
const url = new URL(urlStr);
|
||||||
|
expect(url.pathname).toContain('/api/history/period/2024-01-01T00:00:00Z');
|
||||||
|
expect(url.searchParams.get('filter_entity_id')).toBe('light.living_room');
|
||||||
|
expect(url.searchParams.get('minimal_response')).toBe('true');
|
||||||
|
expect(url.searchParams.get('significant_changes_only')).toBe('true');
|
||||||
|
|
||||||
|
expect(options).toEqual({
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle fetch errors', async () => {
|
test('should handle fetch errors', async () => {
|
||||||
mockFetch.mockRejectedValueOnce(new Error('Network error'));
|
// Setup error response
|
||||||
|
mockFetch = mock(() => Promise.reject(new Error('Network error')));
|
||||||
|
globalThis.fetch = mockFetch;
|
||||||
|
|
||||||
const historyTool = addToolCalls.find(call => call[0].name === 'get_history')?.[0];
|
const historyTool = addToolCalls.find(call => call.name === 'get_history');
|
||||||
expect(historyTool).toBeDefined();
|
expect(historyTool).toBeDefined();
|
||||||
|
|
||||||
if (!historyTool) {
|
if (!historyTool) {
|
||||||
@@ -555,7 +614,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
json: async () => mockScenes
|
json: async () => mockScenes
|
||||||
} as Response);
|
} as Response);
|
||||||
|
|
||||||
const sceneTool = addToolCalls.find(call => call[0].name === 'scene')?.[0];
|
const sceneTool = addToolCalls.find(call => call.name === 'scene');
|
||||||
expect(sceneTool).toBeDefined();
|
expect(sceneTool).toBeDefined();
|
||||||
|
|
||||||
if (!sceneTool) {
|
if (!sceneTool) {
|
||||||
@@ -587,7 +646,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
json: async () => ({})
|
json: async () => ({})
|
||||||
} as Response);
|
} as Response);
|
||||||
|
|
||||||
const sceneTool = addToolCalls.find(call => call[0].name === 'scene')?.[0];
|
const sceneTool = addToolCalls.find(call => call.name === 'scene');
|
||||||
expect(sceneTool).toBeDefined();
|
expect(sceneTool).toBeDefined();
|
||||||
|
|
||||||
if (!sceneTool) {
|
if (!sceneTool) {
|
||||||
@@ -603,11 +662,11 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
expect(result.message).toBe('Successfully activated scene scene.movie_time');
|
expect(result.message).toBe('Successfully activated scene scene.movie_time');
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
`${TEST_HASS_HOST}/api/services/scene/turn_on`,
|
`${TEST_CONFIG.HASS_HOST}/api/services/scene/turn_on`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -625,7 +684,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
json: async () => ({})
|
json: async () => ({})
|
||||||
} as Response);
|
} as Response);
|
||||||
|
|
||||||
const notifyTool = addToolCalls.find(call => call[0].name === 'notify')?.[0];
|
const notifyTool = addToolCalls.find(call => call.name === 'notify');
|
||||||
expect(notifyTool).toBeDefined();
|
expect(notifyTool).toBeDefined();
|
||||||
|
|
||||||
if (!notifyTool) {
|
if (!notifyTool) {
|
||||||
@@ -643,11 +702,11 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
expect(result.message).toBe('Notification sent successfully');
|
expect(result.message).toBe('Notification sent successfully');
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
`${TEST_HASS_HOST}/api/services/notify/mobile_app_phone`,
|
`${TEST_CONFIG.HASS_HOST}/api/services/notify/mobile_app_phone`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -660,12 +719,13 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should use default notification service when no target is specified', async () => {
|
it('should use default notification service when no target is specified', async () => {
|
||||||
mockFetch.mockResolvedValueOnce({
|
// Setup response for this test
|
||||||
ok: true,
|
mockFetch = mock(() => Promise.resolve(new Response(
|
||||||
json: async () => ({})
|
JSON.stringify({})
|
||||||
} as Response);
|
)));
|
||||||
|
globalThis.fetch = mockFetch;
|
||||||
|
|
||||||
const notifyTool = addToolCalls.find(call => call[0].name === 'notify')?.[0];
|
const notifyTool = addToolCalls.find(call => call.name === 'notify');
|
||||||
expect(notifyTool).toBeDefined();
|
expect(notifyTool).toBeDefined();
|
||||||
|
|
||||||
if (!notifyTool) {
|
if (!notifyTool) {
|
||||||
@@ -676,10 +736,11 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
message: 'Test notification'
|
message: 'Test notification'
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
const calls = mockFetch.mock.calls;
|
||||||
`${TEST_HASS_HOST}/api/services/notify/notify`,
|
expect(calls.length).toBeGreaterThan(0);
|
||||||
expect.any(Object)
|
|
||||||
);
|
const [url, _options] = calls[0].args;
|
||||||
|
expect(url.toString()).toBe(`${TEST_CONFIG.HASS_HOST}/api/services/notify/notify`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -709,7 +770,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
json: async () => mockAutomations
|
json: async () => mockAutomations
|
||||||
} as Response);
|
} as Response);
|
||||||
|
|
||||||
const automationTool = addToolCalls.find(call => call[0].name === 'automation')?.[0];
|
const automationTool = addToolCalls.find(call => call.name === 'automation');
|
||||||
expect(automationTool).toBeDefined();
|
expect(automationTool).toBeDefined();
|
||||||
|
|
||||||
if (!automationTool) {
|
if (!automationTool) {
|
||||||
@@ -743,7 +804,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
json: async () => ({})
|
json: async () => ({})
|
||||||
} as Response);
|
} as Response);
|
||||||
|
|
||||||
const automationTool = addToolCalls.find(call => call[0].name === 'automation')?.[0];
|
const automationTool = addToolCalls.find(call => call.name === 'automation');
|
||||||
expect(automationTool).toBeDefined();
|
expect(automationTool).toBeDefined();
|
||||||
|
|
||||||
if (!automationTool) {
|
if (!automationTool) {
|
||||||
@@ -759,11 +820,11 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
expect(result.message).toBe('Successfully toggled automation automation.morning_routine');
|
expect(result.message).toBe('Successfully toggled automation automation.morning_routine');
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
`${TEST_HASS_HOST}/api/services/automation/toggle`,
|
`${TEST_CONFIG.HASS_HOST}/api/services/automation/toggle`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -779,7 +840,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
json: async () => ({})
|
json: async () => ({})
|
||||||
} as Response);
|
} as Response);
|
||||||
|
|
||||||
const automationTool = addToolCalls.find(call => call[0].name === 'automation')?.[0];
|
const automationTool = addToolCalls.find(call => call.name === 'automation');
|
||||||
expect(automationTool).toBeDefined();
|
expect(automationTool).toBeDefined();
|
||||||
|
|
||||||
if (!automationTool) {
|
if (!automationTool) {
|
||||||
@@ -795,11 +856,11 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
expect(result.message).toBe('Successfully triggered automation automation.morning_routine');
|
expect(result.message).toBe('Successfully triggered automation automation.morning_routine');
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
`${TEST_HASS_HOST}/api/services/automation/trigger`,
|
`${TEST_CONFIG.HASS_HOST}/api/services/automation/trigger`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -810,7 +871,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should require automation_id for toggle and trigger actions', async () => {
|
it('should require automation_id for toggle and trigger actions', async () => {
|
||||||
const automationTool = addToolCalls.find(call => call[0].name === 'automation')?.[0];
|
const automationTool = addToolCalls.find(call => call.name === 'automation');
|
||||||
expect(automationTool).toBeDefined();
|
expect(automationTool).toBeDefined();
|
||||||
|
|
||||||
if (!automationTool) {
|
if (!automationTool) {
|
||||||
@@ -858,7 +919,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
json: async () => mockAddons
|
json: async () => mockAddons
|
||||||
} as Response);
|
} as Response);
|
||||||
|
|
||||||
const addonTool = addToolCalls.find(call => call[0].name === 'addon')?.[0];
|
const addonTool = addToolCalls.find(call => call.name === 'addon');
|
||||||
expect(addonTool).toBeDefined();
|
expect(addonTool).toBeDefined();
|
||||||
|
|
||||||
if (!addonTool) {
|
if (!addonTool) {
|
||||||
@@ -879,7 +940,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
json: async () => ({ data: { state: 'installing' } })
|
json: async () => ({ data: { state: 'installing' } })
|
||||||
} as Response);
|
} as Response);
|
||||||
|
|
||||||
const addonTool = addToolCalls.find(call => call[0].name === 'addon')?.[0];
|
const addonTool = addToolCalls.find(call => call.name === 'addon');
|
||||||
expect(addonTool).toBeDefined();
|
expect(addonTool).toBeDefined();
|
||||||
|
|
||||||
if (!addonTool) {
|
if (!addonTool) {
|
||||||
@@ -896,11 +957,11 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
expect(result.message).toBe('Successfully installed add-on core_configurator');
|
expect(result.message).toBe('Successfully installed add-on core_configurator');
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
`${TEST_HASS_HOST}/api/hassio/addons/core_configurator/install`,
|
`${TEST_CONFIG.HASS_HOST}/api/hassio/addons/core_configurator/install`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ version: '5.6.0' })
|
body: JSON.stringify({ version: '5.6.0' })
|
||||||
@@ -931,7 +992,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
json: async () => mockPackages
|
json: async () => mockPackages
|
||||||
} as Response);
|
} as Response);
|
||||||
|
|
||||||
const packageTool = addToolCalls.find(call => call[0].name === 'package')?.[0];
|
const packageTool = addToolCalls.find(call => call.name === 'package');
|
||||||
expect(packageTool).toBeDefined();
|
expect(packageTool).toBeDefined();
|
||||||
|
|
||||||
if (!packageTool) {
|
if (!packageTool) {
|
||||||
@@ -953,7 +1014,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
json: async () => ({})
|
json: async () => ({})
|
||||||
} as Response);
|
} as Response);
|
||||||
|
|
||||||
const packageTool = addToolCalls.find(call => call[0].name === 'package')?.[0];
|
const packageTool = addToolCalls.find(call => call.name === 'package');
|
||||||
expect(packageTool).toBeDefined();
|
expect(packageTool).toBeDefined();
|
||||||
|
|
||||||
if (!packageTool) {
|
if (!packageTool) {
|
||||||
@@ -971,11 +1032,11 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
expect(result.message).toBe('Successfully installed package hacs/integration');
|
expect(result.message).toBe('Successfully installed package hacs/integration');
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
`${TEST_HASS_HOST}/api/hacs/repository/install`,
|
`${TEST_CONFIG.HASS_HOST}/api/hacs/repository/install`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -1016,7 +1077,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
json: async () => ({ automation_id: 'new_automation_1' })
|
json: async () => ({ automation_id: 'new_automation_1' })
|
||||||
} as Response);
|
} as Response);
|
||||||
|
|
||||||
const automationConfigTool = addToolCalls.find(call => call[0].name === 'automation_config')?.[0];
|
const automationConfigTool = addToolCalls.find(call => call.name === 'automation_config');
|
||||||
expect(automationConfigTool).toBeDefined();
|
expect(automationConfigTool).toBeDefined();
|
||||||
|
|
||||||
if (!automationConfigTool) {
|
if (!automationConfigTool) {
|
||||||
@@ -1033,11 +1094,11 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
expect(result.automation_id).toBe('new_automation_1');
|
expect(result.automation_id).toBe('new_automation_1');
|
||||||
|
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
`${TEST_HASS_HOST}/api/config/automation/config`,
|
`${TEST_CONFIG.HASS_HOST}/api/config/automation/config`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(mockAutomationConfig)
|
body: JSON.stringify(mockAutomationConfig)
|
||||||
@@ -1058,7 +1119,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
json: async () => ({ automation_id: 'new_automation_2' })
|
json: async () => ({ automation_id: 'new_automation_2' })
|
||||||
} as Response);
|
} as Response);
|
||||||
|
|
||||||
const automationConfigTool = addToolCalls.find(call => call[0].name === 'automation_config')?.[0];
|
const automationConfigTool = addToolCalls.find(call => call.name === 'automation_config');
|
||||||
expect(automationConfigTool).toBeDefined();
|
expect(automationConfigTool).toBeDefined();
|
||||||
|
|
||||||
if (!automationConfigTool) {
|
if (!automationConfigTool) {
|
||||||
@@ -1076,17 +1137,17 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
|
|
||||||
// Verify both API calls
|
// Verify both API calls
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
`${TEST_HASS_HOST}/api/config/automation/config/automation.test`,
|
`${TEST_CONFIG.HASS_HOST}/api/config/automation/config/automation.test`,
|
||||||
expect.any(Object)
|
expect.any(Object)
|
||||||
);
|
);
|
||||||
|
|
||||||
const duplicateConfig = { ...mockAutomationConfig, alias: 'Test Automation (Copy)' };
|
const duplicateConfig = { ...mockAutomationConfig, alias: 'Test Automation (Copy)' };
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
`${TEST_HASS_HOST}/api/config/automation/config`,
|
`${TEST_CONFIG.HASS_HOST}/api/config/automation/config`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(duplicateConfig)
|
body: JSON.stringify(duplicateConfig)
|
||||||
@@ -1095,7 +1156,7 @@ describe('Home Assistant MCP Server', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should require config for create action', async () => {
|
it('should require config for create action', async () => {
|
||||||
const automationConfigTool = addToolCalls.find(call => call[0].name === 'automation_config')?.[0];
|
const automationConfigTool = addToolCalls.find(call => call.name === 'automation_config');
|
||||||
expect(automationConfigTool).toBeDefined();
|
expect(automationConfigTool).toBeDefined();
|
||||||
|
|
||||||
if (!automationConfigTool) {
|
if (!automationConfigTool) {
|
||||||
|
|||||||
@@ -1,61 +1,81 @@
|
|||||||
import { jest, describe, beforeEach, afterEach, it, expect } from '@jest/globals';
|
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
|
||||||
import express from 'express';
|
import type { Mock } from "bun:test";
|
||||||
import { LiteMCP } from 'litemcp';
|
import type { Express, Application } from 'express';
|
||||||
import { logger } from '../src/utils/logger.js';
|
import type { Logger } from 'winston';
|
||||||
|
|
||||||
|
// Types for our mocks
|
||||||
|
interface MockApp {
|
||||||
|
use: Mock<() => void>;
|
||||||
|
listen: Mock<(port: number, callback: () => void) => { close: Mock<() => void> }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MockLiteMCPInstance {
|
||||||
|
addTool: Mock<() => void>;
|
||||||
|
start: Mock<() => Promise<void>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockLogger = {
|
||||||
|
info: Mock<(message: string) => void>;
|
||||||
|
error: Mock<(message: string) => void>;
|
||||||
|
debug: Mock<(message: string) => void>;
|
||||||
|
};
|
||||||
|
|
||||||
// Mock express
|
// Mock express
|
||||||
jest.mock('express', () => {
|
const mockApp: MockApp = {
|
||||||
const mockApp = {
|
use: mock(() => undefined),
|
||||||
use: jest.fn(),
|
listen: mock((port: number, callback: () => void) => {
|
||||||
listen: jest.fn((port: number, callback: () => void) => {
|
callback();
|
||||||
callback();
|
return { close: mock(() => undefined) };
|
||||||
return { close: jest.fn() };
|
})
|
||||||
})
|
};
|
||||||
};
|
const mockExpress = mock(() => mockApp);
|
||||||
return jest.fn(() => mockApp);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock LiteMCP
|
// Mock LiteMCP instance
|
||||||
jest.mock('litemcp', () => ({
|
const mockLiteMCPInstance: MockLiteMCPInstance = {
|
||||||
LiteMCP: jest.fn(() => ({
|
addTool: mock(() => undefined),
|
||||||
addTool: jest.fn(),
|
start: mock(() => Promise.resolve())
|
||||||
start: jest.fn().mockImplementation(async () => { })
|
};
|
||||||
}))
|
const mockLiteMCP = mock((name: string, version: string) => mockLiteMCPInstance);
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock logger
|
// Mock logger
|
||||||
jest.mock('../src/utils/logger.js', () => ({
|
const mockLogger: MockLogger = {
|
||||||
logger: {
|
info: mock((message: string) => undefined),
|
||||||
info: jest.fn(),
|
error: mock((message: string) => undefined),
|
||||||
error: jest.fn(),
|
debug: mock((message: string) => undefined)
|
||||||
debug: jest.fn()
|
};
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('Server Initialization', () => {
|
describe('Server Initialization', () => {
|
||||||
let originalEnv: NodeJS.ProcessEnv;
|
let originalEnv: NodeJS.ProcessEnv;
|
||||||
let mockApp: ReturnType<typeof express>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Store original environment
|
// Store original environment
|
||||||
originalEnv = { ...process.env };
|
originalEnv = { ...process.env };
|
||||||
|
|
||||||
// Reset all mocks
|
// Setup mocks
|
||||||
jest.clearAllMocks();
|
(globalThis as any).express = mockExpress;
|
||||||
|
(globalThis as any).LiteMCP = mockLiteMCP;
|
||||||
|
(globalThis as any).logger = mockLogger;
|
||||||
|
|
||||||
// Get the mock express app
|
// Reset all mocks
|
||||||
mockApp = express();
|
mockApp.use.mockReset();
|
||||||
|
mockApp.listen.mockReset();
|
||||||
|
mockLogger.info.mockReset();
|
||||||
|
mockLogger.error.mockReset();
|
||||||
|
mockLogger.debug.mockReset();
|
||||||
|
mockLiteMCP.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// Restore original environment
|
// Restore original environment
|
||||||
process.env = originalEnv;
|
process.env = originalEnv;
|
||||||
|
|
||||||
// Clear module cache to ensure fresh imports
|
// Clean up mocks
|
||||||
jest.resetModules();
|
delete (globalThis as any).express;
|
||||||
|
delete (globalThis as any).LiteMCP;
|
||||||
|
delete (globalThis as any).logger;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should start Express server when not in Claude mode', async () => {
|
test('should start Express server when not in Claude mode', async () => {
|
||||||
// Set OpenAI mode
|
// Set OpenAI mode
|
||||||
process.env.PROCESSOR_TYPE = 'openai';
|
process.env.PROCESSOR_TYPE = 'openai';
|
||||||
|
|
||||||
@@ -63,13 +83,15 @@ describe('Server Initialization', () => {
|
|||||||
await import('../src/index.js');
|
await import('../src/index.js');
|
||||||
|
|
||||||
// Verify Express server was initialized
|
// Verify Express server was initialized
|
||||||
expect(express).toHaveBeenCalled();
|
expect(mockExpress.mock.calls.length).toBeGreaterThan(0);
|
||||||
expect(mockApp.use).toHaveBeenCalled();
|
expect(mockApp.use.mock.calls.length).toBeGreaterThan(0);
|
||||||
expect(mockApp.listen).toHaveBeenCalled();
|
expect(mockApp.listen.mock.calls.length).toBeGreaterThan(0);
|
||||||
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Server is running on port'));
|
|
||||||
|
const infoMessages = mockLogger.info.mock.calls.map(([msg]) => msg);
|
||||||
|
expect(infoMessages.some(msg => msg.includes('Server is running on port'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not start Express server in Claude mode', async () => {
|
test('should not start Express server in Claude mode', async () => {
|
||||||
// Set Claude mode
|
// Set Claude mode
|
||||||
process.env.PROCESSOR_TYPE = 'claude';
|
process.env.PROCESSOR_TYPE = 'claude';
|
||||||
|
|
||||||
@@ -77,28 +99,38 @@ describe('Server Initialization', () => {
|
|||||||
await import('../src/index.js');
|
await import('../src/index.js');
|
||||||
|
|
||||||
// Verify Express server was not initialized
|
// Verify Express server was not initialized
|
||||||
expect(express).not.toHaveBeenCalled();
|
expect(mockExpress.mock.calls.length).toBe(0);
|
||||||
expect(mockApp.use).not.toHaveBeenCalled();
|
expect(mockApp.use.mock.calls.length).toBe(0);
|
||||||
expect(mockApp.listen).not.toHaveBeenCalled();
|
expect(mockApp.listen.mock.calls.length).toBe(0);
|
||||||
expect(logger.info).toHaveBeenCalledWith('Running in Claude mode - Express server disabled');
|
|
||||||
|
const infoMessages = mockLogger.info.mock.calls.map(([msg]) => msg);
|
||||||
|
expect(infoMessages).toContain('Running in Claude mode - Express server disabled');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should initialize LiteMCP in both modes', async () => {
|
test('should initialize LiteMCP in both modes', async () => {
|
||||||
// Test OpenAI mode
|
// Test OpenAI mode
|
||||||
process.env.PROCESSOR_TYPE = 'openai';
|
process.env.PROCESSOR_TYPE = 'openai';
|
||||||
await import('../src/index.js');
|
await import('../src/index.js');
|
||||||
expect(LiteMCP).toHaveBeenCalledWith('home-assistant', expect.any(String));
|
|
||||||
|
|
||||||
// Reset modules
|
expect(mockLiteMCP.mock.calls.length).toBeGreaterThan(0);
|
||||||
jest.resetModules();
|
const [name, version] = mockLiteMCP.mock.calls[0] ?? [];
|
||||||
|
expect(name).toBe('home-assistant');
|
||||||
|
expect(typeof version).toBe('string');
|
||||||
|
|
||||||
|
// Reset for next test
|
||||||
|
mockLiteMCP.mockReset();
|
||||||
|
|
||||||
// Test Claude mode
|
// Test Claude mode
|
||||||
process.env.PROCESSOR_TYPE = 'claude';
|
process.env.PROCESSOR_TYPE = 'claude';
|
||||||
await import('../src/index.js');
|
await import('../src/index.js');
|
||||||
expect(LiteMCP).toHaveBeenCalledWith('home-assistant', expect.any(String));
|
|
||||||
|
expect(mockLiteMCP.mock.calls.length).toBeGreaterThan(0);
|
||||||
|
const [name2, version2] = mockLiteMCP.mock.calls[0] ?? [];
|
||||||
|
expect(name2).toBe('home-assistant');
|
||||||
|
expect(typeof version2).toBe('string');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle missing PROCESSOR_TYPE (default to Express server)', async () => {
|
test('should handle missing PROCESSOR_TYPE (default to Express server)', async () => {
|
||||||
// Remove PROCESSOR_TYPE
|
// Remove PROCESSOR_TYPE
|
||||||
delete process.env.PROCESSOR_TYPE;
|
delete process.env.PROCESSOR_TYPE;
|
||||||
|
|
||||||
@@ -106,9 +138,11 @@ describe('Server Initialization', () => {
|
|||||||
await import('../src/index.js');
|
await import('../src/index.js');
|
||||||
|
|
||||||
// Verify Express server was initialized (default behavior)
|
// Verify Express server was initialized (default behavior)
|
||||||
expect(express).toHaveBeenCalled();
|
expect(mockExpress.mock.calls.length).toBeGreaterThan(0);
|
||||||
expect(mockApp.use).toHaveBeenCalled();
|
expect(mockApp.use.mock.calls.length).toBeGreaterThan(0);
|
||||||
expect(mockApp.listen).toHaveBeenCalled();
|
expect(mockApp.listen.mock.calls.length).toBeGreaterThan(0);
|
||||||
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Server is running on port'));
|
|
||||||
|
const infoMessages = mockLogger.info.mock.calls.map(([msg]) => msg);
|
||||||
|
expect(infoMessages.some(msg => msg.includes('Server is running on port'))).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
327
__tests__/speech/speechToText.test.ts
Normal file
327
__tests__/speech/speechToText.test.ts
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
import { SpeechToText, TranscriptionResult, WakeWordEvent, TranscriptionError, TranscriptionOptions } from '../../src/speech/speechToText';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import { describe, expect, beforeEach, afterEach, it, mock, spyOn } from 'bun:test';
|
||||||
|
|
||||||
|
// Mock child_process spawn
|
||||||
|
const spawnMock = mock((cmd: string, args: string[]) => ({
|
||||||
|
stdout: new EventEmitter(),
|
||||||
|
stderr: new EventEmitter(),
|
||||||
|
on: (event: string, cb: (code: number) => void) => {
|
||||||
|
if (event === 'close') setTimeout(() => cb(0), 0);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('SpeechToText', () => {
|
||||||
|
let speechToText: SpeechToText;
|
||||||
|
const testAudioDir = path.join(import.meta.dir, 'test_audio');
|
||||||
|
const mockConfig = {
|
||||||
|
containerName: 'test-whisper',
|
||||||
|
modelPath: '/models/whisper',
|
||||||
|
modelType: 'base.en'
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
speechToText = new SpeechToText(mockConfig);
|
||||||
|
// Create test audio directory if it doesn't exist
|
||||||
|
if (!fs.existsSync(testAudioDir)) {
|
||||||
|
fs.mkdirSync(testAudioDir, { recursive: true });
|
||||||
|
}
|
||||||
|
// Reset spawn mock
|
||||||
|
spawnMock.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
speechToText.stopWakeWordDetection();
|
||||||
|
// Clean up test files
|
||||||
|
if (fs.existsSync(testAudioDir)) {
|
||||||
|
fs.rmSync(testAudioDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Initialization', () => {
|
||||||
|
it('should create instance with default config', () => {
|
||||||
|
const instance = new SpeechToText({ modelPath: '/models/whisper', modelType: 'base.en' });
|
||||||
|
expect(instance instanceof EventEmitter).toBe(true);
|
||||||
|
expect(instance instanceof SpeechToText).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize successfully', async () => {
|
||||||
|
const initSpy = spyOn(speechToText, 'initialize');
|
||||||
|
await speechToText.initialize();
|
||||||
|
expect(initSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not initialize twice', async () => {
|
||||||
|
await speechToText.initialize();
|
||||||
|
const initSpy = spyOn(speechToText, 'initialize');
|
||||||
|
await speechToText.initialize();
|
||||||
|
expect(initSpy.mock.calls.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Health Check', () => {
|
||||||
|
it('should return true when Docker container is running', async () => {
|
||||||
|
const mockProcess = {
|
||||||
|
stdout: new EventEmitter(),
|
||||||
|
stderr: new EventEmitter(),
|
||||||
|
on: (event: string, cb: (code: number) => void) => {
|
||||||
|
if (event === 'close') setTimeout(() => cb(0), 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
spawnMock.mockImplementation(() => mockProcess);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
mockProcess.stdout.emit('data', Buffer.from('Up 2 hours'));
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const result = await speechToText.checkHealth();
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when Docker container is not running', async () => {
|
||||||
|
const mockProcess = {
|
||||||
|
stdout: new EventEmitter(),
|
||||||
|
stderr: new EventEmitter(),
|
||||||
|
on: (event: string, cb: (code: number) => void) => {
|
||||||
|
if (event === 'close') setTimeout(() => cb(1), 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
spawnMock.mockImplementation(() => mockProcess);
|
||||||
|
|
||||||
|
const result = await speechToText.checkHealth();
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Docker command errors', async () => {
|
||||||
|
spawnMock.mockImplementation(() => {
|
||||||
|
throw new Error('Docker not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await speechToText.checkHealth();
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Wake Word Detection', () => {
|
||||||
|
it('should detect wake word and emit event', async () => {
|
||||||
|
const testFile = path.join(testAudioDir, 'wake_word_test_123456.wav');
|
||||||
|
const testMetadata = `${testFile}.json`;
|
||||||
|
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
speechToText.startWakeWordDetection(testAudioDir);
|
||||||
|
|
||||||
|
speechToText.on('wake_word', (event: WakeWordEvent) => {
|
||||||
|
expect(event).toBeDefined();
|
||||||
|
expect(event.audioFile).toBe(testFile);
|
||||||
|
expect(event.metadataFile).toBe(testMetadata);
|
||||||
|
expect(event.timestamp).toBe('123456');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a test audio file to trigger the event
|
||||||
|
fs.writeFileSync(testFile, 'test audio content');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle non-wake-word files', async () => {
|
||||||
|
const testFile = path.join(testAudioDir, 'regular_audio.wav');
|
||||||
|
let eventEmitted = false;
|
||||||
|
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
speechToText.startWakeWordDetection(testAudioDir);
|
||||||
|
|
||||||
|
speechToText.on('wake_word', () => {
|
||||||
|
eventEmitted = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(testFile, 'test audio content');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(eventEmitted).toBe(false);
|
||||||
|
resolve();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Audio Transcription', () => {
|
||||||
|
const mockTranscriptionResult: TranscriptionResult = {
|
||||||
|
text: 'Hello world',
|
||||||
|
segments: [{
|
||||||
|
text: 'Hello world',
|
||||||
|
start: 0,
|
||||||
|
end: 1,
|
||||||
|
confidence: 0.95
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should transcribe audio successfully', async () => {
|
||||||
|
const mockProcess = {
|
||||||
|
stdout: new EventEmitter(),
|
||||||
|
stderr: new EventEmitter(),
|
||||||
|
on: (event: string, cb: (code: number) => void) => {
|
||||||
|
if (event === 'close') setTimeout(() => cb(0), 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
spawnMock.mockImplementation(() => mockProcess);
|
||||||
|
|
||||||
|
const transcriptionPromise = speechToText.transcribeAudio('/test/audio.wav');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
mockProcess.stdout.emit('data', Buffer.from(JSON.stringify(mockTranscriptionResult)));
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const result = await transcriptionPromise;
|
||||||
|
expect(result).toEqual(mockTranscriptionResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle transcription errors', async () => {
|
||||||
|
const mockProcess = {
|
||||||
|
stdout: new EventEmitter(),
|
||||||
|
stderr: new EventEmitter(),
|
||||||
|
on: (event: string, cb: (code: number) => void) => {
|
||||||
|
if (event === 'close') setTimeout(() => cb(1), 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
spawnMock.mockImplementation(() => mockProcess);
|
||||||
|
|
||||||
|
const transcriptionPromise = speechToText.transcribeAudio('/test/audio.wav');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
mockProcess.stderr.emit('data', Buffer.from('Transcription failed'));
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
await expect(transcriptionPromise).rejects.toThrow(TranscriptionError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle invalid JSON output', async () => {
|
||||||
|
const mockProcess = {
|
||||||
|
stdout: new EventEmitter(),
|
||||||
|
stderr: new EventEmitter(),
|
||||||
|
on: (event: string, cb: (code: number) => void) => {
|
||||||
|
if (event === 'close') setTimeout(() => cb(0), 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
spawnMock.mockImplementation(() => mockProcess);
|
||||||
|
|
||||||
|
const transcriptionPromise = speechToText.transcribeAudio('/test/audio.wav');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
mockProcess.stdout.emit('data', Buffer.from('Invalid JSON'));
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
await expect(transcriptionPromise).rejects.toThrow(TranscriptionError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass correct transcription options', async () => {
|
||||||
|
const options: TranscriptionOptions = {
|
||||||
|
model: 'large-v2',
|
||||||
|
language: 'en',
|
||||||
|
temperature: 0.5,
|
||||||
|
beamSize: 3,
|
||||||
|
patience: 2,
|
||||||
|
device: 'cuda'
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockProcess = {
|
||||||
|
stdout: new EventEmitter(),
|
||||||
|
stderr: new EventEmitter(),
|
||||||
|
on: (event: string, cb: (code: number) => void) => {
|
||||||
|
if (event === 'close') setTimeout(() => cb(0), 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
spawnMock.mockImplementation(() => mockProcess);
|
||||||
|
|
||||||
|
const transcriptionPromise = speechToText.transcribeAudio('/test/audio.wav', options);
|
||||||
|
|
||||||
|
const expectedArgs = [
|
||||||
|
'exec',
|
||||||
|
mockConfig.containerName,
|
||||||
|
'fast-whisper',
|
||||||
|
'--model', options.model,
|
||||||
|
'--language', options.language,
|
||||||
|
'--temperature', String(options.temperature ?? 0),
|
||||||
|
'--beam-size', String(options.beamSize ?? 5),
|
||||||
|
'--patience', String(options.patience ?? 1),
|
||||||
|
'--device', options.device
|
||||||
|
].filter((arg): arg is string => arg !== undefined);
|
||||||
|
|
||||||
|
const mockCalls = spawnMock.mock.calls;
|
||||||
|
expect(mockCalls.length).toBe(1);
|
||||||
|
const [cmd, args] = mockCalls[0].args;
|
||||||
|
expect(cmd).toBe('docker');
|
||||||
|
expect(expectedArgs.every(arg => args.includes(arg))).toBe(true);
|
||||||
|
|
||||||
|
await transcriptionPromise.catch(() => { });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Event Handling', () => {
|
||||||
|
it('should emit progress events', async () => {
|
||||||
|
const mockProcess = {
|
||||||
|
stdout: new EventEmitter(),
|
||||||
|
stderr: new EventEmitter(),
|
||||||
|
on: (event: string, cb: (code: number) => void) => {
|
||||||
|
if (event === 'close') setTimeout(() => cb(0), 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
spawnMock.mockImplementation(() => mockProcess);
|
||||||
|
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
const progressEvents: any[] = [];
|
||||||
|
speechToText.on('progress', (event) => {
|
||||||
|
progressEvents.push(event);
|
||||||
|
if (progressEvents.length === 2) {
|
||||||
|
expect(progressEvents).toEqual([
|
||||||
|
{ type: 'stdout', data: 'Processing' },
|
||||||
|
{ type: 'stderr', data: 'Loading model' }
|
||||||
|
]);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
void speechToText.transcribeAudio('/test/audio.wav');
|
||||||
|
|
||||||
|
mockProcess.stdout.emit('data', Buffer.from('Processing'));
|
||||||
|
mockProcess.stderr.emit('data', Buffer.from('Loading model'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit error events', async () => {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
speechToText.on('error', (error) => {
|
||||||
|
expect(error instanceof Error).toBe(true);
|
||||||
|
expect(error.message).toBe('Test error');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
speechToText.emit('error', new Error('Test error'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Cleanup', () => {
|
||||||
|
it('should stop wake word detection', () => {
|
||||||
|
speechToText.startWakeWordDetection(testAudioDir);
|
||||||
|
speechToText.stopWakeWordDetection();
|
||||||
|
// Verify no more file watching events are processed
|
||||||
|
const testFile = path.join(testAudioDir, 'wake_word_test_123456.wav');
|
||||||
|
let eventEmitted = false;
|
||||||
|
speechToText.on('wake_word', () => {
|
||||||
|
eventEmitted = true;
|
||||||
|
});
|
||||||
|
fs.writeFileSync(testFile, 'test audio content');
|
||||||
|
expect(eventEmitted).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clean up resources on shutdown', async () => {
|
||||||
|
await speechToText.initialize();
|
||||||
|
const shutdownSpy = spyOn(speechToText, 'shutdown');
|
||||||
|
await speechToText.shutdown();
|
||||||
|
expect(shutdownSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
202
__tests__/tools/automation-config.test.ts
Normal file
202
__tests__/tools/automation-config.test.ts
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
|
||||||
|
import {
|
||||||
|
type MockLiteMCPInstance,
|
||||||
|
type Tool,
|
||||||
|
type TestResponse,
|
||||||
|
TEST_CONFIG,
|
||||||
|
createMockLiteMCPInstance,
|
||||||
|
setupTestEnvironment,
|
||||||
|
cleanupMocks,
|
||||||
|
createMockResponse,
|
||||||
|
getMockCallArgs
|
||||||
|
} from '../utils/test-utils';
|
||||||
|
|
||||||
|
describe('Automation Configuration Tools', () => {
|
||||||
|
let liteMcpInstance: MockLiteMCPInstance;
|
||||||
|
let addToolCalls: Tool[];
|
||||||
|
let mocks: ReturnType<typeof setupTestEnvironment>;
|
||||||
|
|
||||||
|
const mockAutomationConfig = {
|
||||||
|
alias: 'Test Automation',
|
||||||
|
description: 'Test automation description',
|
||||||
|
mode: 'single',
|
||||||
|
trigger: [
|
||||||
|
{
|
||||||
|
platform: 'state',
|
||||||
|
entity_id: 'binary_sensor.motion',
|
||||||
|
to: 'on'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
action: [
|
||||||
|
{
|
||||||
|
service: 'light.turn_on',
|
||||||
|
target: {
|
||||||
|
entity_id: 'light.living_room'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Setup test environment
|
||||||
|
mocks = setupTestEnvironment();
|
||||||
|
liteMcpInstance = createMockLiteMCPInstance();
|
||||||
|
|
||||||
|
// Import the module which will execute the main function
|
||||||
|
await import('../../src/index.js');
|
||||||
|
|
||||||
|
// Get the mock instance and tool calls
|
||||||
|
addToolCalls = liteMcpInstance.addTool.mock.calls.map(call => call.args[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupMocks({ liteMcpInstance, ...mocks });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('automation_config tool', () => {
|
||||||
|
test('should successfully create an automation', async () => {
|
||||||
|
// Setup response
|
||||||
|
mocks.mockFetch = mock(() => Promise.resolve(createMockResponse({
|
||||||
|
automation_id: 'new_automation_1'
|
||||||
|
})));
|
||||||
|
globalThis.fetch = mocks.mockFetch;
|
||||||
|
|
||||||
|
const automationConfigTool = addToolCalls.find(tool => tool.name === 'automation_config');
|
||||||
|
expect(automationConfigTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!automationConfigTool) {
|
||||||
|
throw new Error('automation_config tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await automationConfigTool.execute({
|
||||||
|
action: 'create',
|
||||||
|
config: mockAutomationConfig
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.message).toBe('Successfully created automation');
|
||||||
|
expect(result.automation_id).toBe('new_automation_1');
|
||||||
|
|
||||||
|
// Verify the fetch call
|
||||||
|
type FetchArgs = [url: string, init: RequestInit];
|
||||||
|
const args = getMockCallArgs<FetchArgs>(mocks.mockFetch);
|
||||||
|
expect(args).toBeDefined();
|
||||||
|
|
||||||
|
if (!args) {
|
||||||
|
throw new Error('No fetch calls recorded');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [urlStr, options] = args;
|
||||||
|
expect(urlStr).toBe(`${TEST_CONFIG.HASS_HOST}/api/config/automation/config`);
|
||||||
|
expect(options).toEqual({
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(mockAutomationConfig)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should successfully duplicate an automation', async () => {
|
||||||
|
// Setup responses for get and create
|
||||||
|
let callCount = 0;
|
||||||
|
mocks.mockFetch = mock(() => {
|
||||||
|
callCount++;
|
||||||
|
return Promise.resolve(
|
||||||
|
callCount === 1
|
||||||
|
? createMockResponse(mockAutomationConfig)
|
||||||
|
: createMockResponse({ automation_id: 'new_automation_2' })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
globalThis.fetch = mocks.mockFetch;
|
||||||
|
|
||||||
|
const automationConfigTool = addToolCalls.find(tool => tool.name === 'automation_config');
|
||||||
|
expect(automationConfigTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!automationConfigTool) {
|
||||||
|
throw new Error('automation_config tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await automationConfigTool.execute({
|
||||||
|
action: 'duplicate',
|
||||||
|
automation_id: 'automation.test'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.message).toBe('Successfully duplicated automation automation.test');
|
||||||
|
expect(result.new_automation_id).toBe('new_automation_2');
|
||||||
|
|
||||||
|
// Verify both API calls
|
||||||
|
type FetchArgs = [url: string, init: RequestInit];
|
||||||
|
const calls = mocks.mockFetch.mock.calls;
|
||||||
|
expect(calls.length).toBe(2);
|
||||||
|
|
||||||
|
// Verify get call
|
||||||
|
const getArgs = getMockCallArgs<FetchArgs>(mocks.mockFetch, 0);
|
||||||
|
expect(getArgs).toBeDefined();
|
||||||
|
if (!getArgs) throw new Error('No get call recorded');
|
||||||
|
|
||||||
|
const [getUrl, getOptions] = getArgs;
|
||||||
|
expect(getUrl).toBe(`${TEST_CONFIG.HASS_HOST}/api/config/automation/config/automation.test`);
|
||||||
|
expect(getOptions).toEqual({
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify create call
|
||||||
|
const createArgs = getMockCallArgs<FetchArgs>(mocks.mockFetch, 1);
|
||||||
|
expect(createArgs).toBeDefined();
|
||||||
|
if (!createArgs) throw new Error('No create call recorded');
|
||||||
|
|
||||||
|
const [createUrl, createOptions] = createArgs;
|
||||||
|
expect(createUrl).toBe(`${TEST_CONFIG.HASS_HOST}/api/config/automation/config`);
|
||||||
|
expect(createOptions).toEqual({
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...mockAutomationConfig,
|
||||||
|
alias: 'Test Automation (Copy)'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should require config for create action', async () => {
|
||||||
|
const automationConfigTool = addToolCalls.find(tool => tool.name === 'automation_config');
|
||||||
|
expect(automationConfigTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!automationConfigTool) {
|
||||||
|
throw new Error('automation_config tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await automationConfigTool.execute({
|
||||||
|
action: 'create'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.message).toBe('Configuration is required for creating automation');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should require automation_id for update action', async () => {
|
||||||
|
const automationConfigTool = addToolCalls.find(tool => tool.name === 'automation_config');
|
||||||
|
expect(automationConfigTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!automationConfigTool) {
|
||||||
|
throw new Error('automation_config tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await automationConfigTool.execute({
|
||||||
|
action: 'update',
|
||||||
|
config: mockAutomationConfig
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.message).toBe('Automation ID and configuration are required for updating automation');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
190
__tests__/tools/automation.test.ts
Normal file
190
__tests__/tools/automation.test.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
|
||||||
|
import {
|
||||||
|
type MockLiteMCPInstance,
|
||||||
|
type Tool,
|
||||||
|
type TestResponse,
|
||||||
|
TEST_CONFIG,
|
||||||
|
createMockLiteMCPInstance,
|
||||||
|
setupTestEnvironment,
|
||||||
|
cleanupMocks,
|
||||||
|
createMockResponse,
|
||||||
|
getMockCallArgs
|
||||||
|
} from '../utils/test-utils';
|
||||||
|
|
||||||
|
describe('Automation Tools', () => {
|
||||||
|
let liteMcpInstance: MockLiteMCPInstance;
|
||||||
|
let addToolCalls: Tool[];
|
||||||
|
let mocks: ReturnType<typeof setupTestEnvironment>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Setup test environment
|
||||||
|
mocks = setupTestEnvironment();
|
||||||
|
liteMcpInstance = createMockLiteMCPInstance();
|
||||||
|
|
||||||
|
// Import the module which will execute the main function
|
||||||
|
await import('../../src/index.js');
|
||||||
|
|
||||||
|
// Get the mock instance and tool calls
|
||||||
|
addToolCalls = liteMcpInstance.addTool.mock.calls.map(call => call.args[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupMocks({ liteMcpInstance, ...mocks });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('automation tool', () => {
|
||||||
|
const mockAutomations = [
|
||||||
|
{
|
||||||
|
entity_id: 'automation.morning_routine',
|
||||||
|
state: 'on',
|
||||||
|
attributes: {
|
||||||
|
friendly_name: 'Morning Routine',
|
||||||
|
last_triggered: '2024-01-01T07:00:00Z'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entity_id: 'automation.night_mode',
|
||||||
|
state: 'off',
|
||||||
|
attributes: {
|
||||||
|
friendly_name: 'Night Mode',
|
||||||
|
last_triggered: '2024-01-01T22:00:00Z'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
test('should successfully list automations', async () => {
|
||||||
|
// Setup response
|
||||||
|
mocks.mockFetch = mock(() => Promise.resolve(createMockResponse(mockAutomations)));
|
||||||
|
globalThis.fetch = mocks.mockFetch;
|
||||||
|
|
||||||
|
const automationTool = addToolCalls.find(tool => tool.name === 'automation');
|
||||||
|
expect(automationTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!automationTool) {
|
||||||
|
throw new Error('automation tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await automationTool.execute({
|
||||||
|
action: 'list'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.automations).toEqual([
|
||||||
|
{
|
||||||
|
entity_id: 'automation.morning_routine',
|
||||||
|
name: 'Morning Routine',
|
||||||
|
state: 'on',
|
||||||
|
last_triggered: '2024-01-01T07:00:00Z'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entity_id: 'automation.night_mode',
|
||||||
|
name: 'Night Mode',
|
||||||
|
state: 'off',
|
||||||
|
last_triggered: '2024-01-01T22:00:00Z'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should successfully toggle an automation', async () => {
|
||||||
|
// Setup response
|
||||||
|
mocks.mockFetch = mock(() => Promise.resolve(createMockResponse({})));
|
||||||
|
globalThis.fetch = mocks.mockFetch;
|
||||||
|
|
||||||
|
const automationTool = addToolCalls.find(tool => tool.name === 'automation');
|
||||||
|
expect(automationTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!automationTool) {
|
||||||
|
throw new Error('automation tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await automationTool.execute({
|
||||||
|
action: 'toggle',
|
||||||
|
automation_id: 'automation.morning_routine'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.message).toBe('Successfully toggled automation automation.morning_routine');
|
||||||
|
|
||||||
|
// Verify the fetch call
|
||||||
|
type FetchArgs = [url: string, init: RequestInit];
|
||||||
|
const args = getMockCallArgs<FetchArgs>(mocks.mockFetch);
|
||||||
|
expect(args).toBeDefined();
|
||||||
|
|
||||||
|
if (!args) {
|
||||||
|
throw new Error('No fetch calls recorded');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [urlStr, options] = args;
|
||||||
|
expect(urlStr).toBe(`${TEST_CONFIG.HASS_HOST}/api/services/automation/toggle`);
|
||||||
|
expect(options).toEqual({
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
entity_id: 'automation.morning_routine'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should successfully trigger an automation', async () => {
|
||||||
|
// Setup response
|
||||||
|
mocks.mockFetch = mock(() => Promise.resolve(createMockResponse({})));
|
||||||
|
globalThis.fetch = mocks.mockFetch;
|
||||||
|
|
||||||
|
const automationTool = addToolCalls.find(tool => tool.name === 'automation');
|
||||||
|
expect(automationTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!automationTool) {
|
||||||
|
throw new Error('automation tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await automationTool.execute({
|
||||||
|
action: 'trigger',
|
||||||
|
automation_id: 'automation.morning_routine'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.message).toBe('Successfully triggered automation automation.morning_routine');
|
||||||
|
|
||||||
|
// Verify the fetch call
|
||||||
|
type FetchArgs = [url: string, init: RequestInit];
|
||||||
|
const args = getMockCallArgs<FetchArgs>(mocks.mockFetch);
|
||||||
|
expect(args).toBeDefined();
|
||||||
|
|
||||||
|
if (!args) {
|
||||||
|
throw new Error('No fetch calls recorded');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [urlStr, options] = args;
|
||||||
|
expect(urlStr).toBe(`${TEST_CONFIG.HASS_HOST}/api/services/automation/trigger`);
|
||||||
|
expect(options).toEqual({
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
entity_id: 'automation.morning_routine'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should require automation_id for toggle and trigger actions', async () => {
|
||||||
|
const automationTool = addToolCalls.find(tool => tool.name === 'automation');
|
||||||
|
expect(automationTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!automationTool) {
|
||||||
|
throw new Error('automation tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await automationTool.execute({
|
||||||
|
action: 'toggle'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.message).toBe('Automation ID is required for toggle and trigger actions');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
361
__tests__/tools/device-control.test.ts
Normal file
361
__tests__/tools/device-control.test.ts
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
|
||||||
|
import {
|
||||||
|
type MockLiteMCPInstance,
|
||||||
|
type Tool,
|
||||||
|
type TestResponse,
|
||||||
|
TEST_CONFIG,
|
||||||
|
createMockLiteMCPInstance,
|
||||||
|
createMockServices,
|
||||||
|
setupTestEnvironment,
|
||||||
|
cleanupMocks,
|
||||||
|
createMockResponse,
|
||||||
|
getMockCallArgs
|
||||||
|
} from '../utils/test-utils';
|
||||||
|
|
||||||
|
describe('Device Control Tools', () => {
|
||||||
|
let liteMcpInstance: MockLiteMCPInstance;
|
||||||
|
let addToolCalls: Tool[];
|
||||||
|
let mocks: ReturnType<typeof setupTestEnvironment>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Setup test environment
|
||||||
|
mocks = setupTestEnvironment();
|
||||||
|
liteMcpInstance = createMockLiteMCPInstance();
|
||||||
|
|
||||||
|
// Import the module which will execute the main function
|
||||||
|
await import('../../src/index.js');
|
||||||
|
|
||||||
|
// Get the mock instance and tool calls
|
||||||
|
addToolCalls = liteMcpInstance.addTool.mock.calls.map(call => call.args[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupMocks({ liteMcpInstance, ...mocks });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('list_devices tool', () => {
|
||||||
|
test('should successfully list devices', async () => {
|
||||||
|
const mockDevices = [
|
||||||
|
{
|
||||||
|
entity_id: 'light.living_room',
|
||||||
|
state: 'on',
|
||||||
|
attributes: { brightness: 255 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entity_id: 'climate.bedroom',
|
||||||
|
state: 'heat',
|
||||||
|
attributes: { temperature: 22 }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Setup response
|
||||||
|
mocks.mockFetch = mock(() => Promise.resolve(createMockResponse(mockDevices)));
|
||||||
|
globalThis.fetch = mocks.mockFetch;
|
||||||
|
|
||||||
|
const listDevicesTool = addToolCalls.find(tool => tool.name === 'list_devices');
|
||||||
|
expect(listDevicesTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!listDevicesTool) {
|
||||||
|
throw new Error('list_devices tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await listDevicesTool.execute({}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.devices).toEqual({
|
||||||
|
light: [{
|
||||||
|
entity_id: 'light.living_room',
|
||||||
|
state: 'on',
|
||||||
|
attributes: { brightness: 255 }
|
||||||
|
}],
|
||||||
|
climate: [{
|
||||||
|
entity_id: 'climate.bedroom',
|
||||||
|
state: 'heat',
|
||||||
|
attributes: { temperature: 22 }
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle fetch errors', async () => {
|
||||||
|
// Setup error response
|
||||||
|
mocks.mockFetch = mock(() => Promise.reject(new Error('Network error')));
|
||||||
|
globalThis.fetch = mocks.mockFetch;
|
||||||
|
|
||||||
|
const listDevicesTool = addToolCalls.find(tool => tool.name === 'list_devices');
|
||||||
|
expect(listDevicesTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!listDevicesTool) {
|
||||||
|
throw new Error('list_devices tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await listDevicesTool.execute({}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.message).toBe('Network error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('control tool', () => {
|
||||||
|
test('should successfully control a light device', async () => {
|
||||||
|
// Setup response
|
||||||
|
mocks.mockFetch = mock(() => Promise.resolve(createMockResponse({})));
|
||||||
|
globalThis.fetch = mocks.mockFetch;
|
||||||
|
|
||||||
|
const controlTool = addToolCalls.find(tool => tool.name === 'control');
|
||||||
|
expect(controlTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!controlTool) {
|
||||||
|
throw new Error('control tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await controlTool.execute({
|
||||||
|
command: 'turn_on',
|
||||||
|
entity_id: 'light.living_room',
|
||||||
|
brightness: 255
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.message).toBe('Successfully executed turn_on for light.living_room');
|
||||||
|
|
||||||
|
// Verify the fetch call
|
||||||
|
const calls = mocks.mockFetch.mock.calls;
|
||||||
|
expect(calls.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
type FetchArgs = [url: string, init: RequestInit];
|
||||||
|
const args = getMockCallArgs<FetchArgs>(mocks.mockFetch);
|
||||||
|
expect(args).toBeDefined();
|
||||||
|
|
||||||
|
if (!args) {
|
||||||
|
throw new Error('No fetch calls recorded');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [urlStr, options] = args;
|
||||||
|
expect(urlStr).toBe(`${TEST_CONFIG.HASS_HOST}/api/services/light/turn_on`);
|
||||||
|
expect(options).toEqual({
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
entity_id: 'light.living_room',
|
||||||
|
brightness: 255
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle unsupported domains', async () => {
|
||||||
|
const controlTool = addToolCalls.find(tool => tool.name === 'control');
|
||||||
|
expect(controlTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!controlTool) {
|
||||||
|
throw new Error('control tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await controlTool.execute({
|
||||||
|
command: 'turn_on',
|
||||||
|
entity_id: 'unsupported.device'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.message).toBe('Unsupported domain: unsupported');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle service call errors', async () => {
|
||||||
|
// Setup error response
|
||||||
|
mocks.mockFetch = mock(() => Promise.resolve(new Response(null, {
|
||||||
|
status: 503,
|
||||||
|
statusText: 'Service unavailable'
|
||||||
|
})));
|
||||||
|
globalThis.fetch = mocks.mockFetch;
|
||||||
|
|
||||||
|
const controlTool = addToolCalls.find(tool => tool.name === 'control');
|
||||||
|
expect(controlTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!controlTool) {
|
||||||
|
throw new Error('control tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await controlTool.execute({
|
||||||
|
command: 'turn_on',
|
||||||
|
entity_id: 'light.living_room'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.message).toContain('Failed to execute turn_on for light.living_room');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle climate device controls', async () => {
|
||||||
|
// Setup response
|
||||||
|
mocks.mockFetch = mock(() => Promise.resolve(createMockResponse({})));
|
||||||
|
globalThis.fetch = mocks.mockFetch;
|
||||||
|
|
||||||
|
const controlTool = addToolCalls.find(tool => tool.name === 'control');
|
||||||
|
expect(controlTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!controlTool) {
|
||||||
|
throw new Error('control tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await controlTool.execute({
|
||||||
|
command: 'set_temperature',
|
||||||
|
entity_id: 'climate.bedroom',
|
||||||
|
temperature: 22,
|
||||||
|
target_temp_high: 24,
|
||||||
|
target_temp_low: 20
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.message).toBe('Successfully executed set_temperature for climate.bedroom');
|
||||||
|
|
||||||
|
// Verify the fetch call
|
||||||
|
const calls = mocks.mockFetch.mock.calls;
|
||||||
|
expect(calls.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
type FetchArgs = [url: string, init: RequestInit];
|
||||||
|
const args = getMockCallArgs<FetchArgs>(mocks.mockFetch);
|
||||||
|
expect(args).toBeDefined();
|
||||||
|
|
||||||
|
if (!args) {
|
||||||
|
throw new Error('No fetch calls recorded');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [urlStr, options] = args;
|
||||||
|
expect(urlStr).toBe(`${TEST_CONFIG.HASS_HOST}/api/services/climate/set_temperature`);
|
||||||
|
expect(options).toEqual({
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
entity_id: 'climate.bedroom',
|
||||||
|
temperature: 22,
|
||||||
|
target_temp_high: 24,
|
||||||
|
target_temp_low: 20
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('device_control tool', () => {
|
||||||
|
test('should successfully control a device', async () => {
|
||||||
|
// Setup response
|
||||||
|
mocks.mockFetch = mock(() => Promise.resolve(createMockResponse({ success: true })));
|
||||||
|
globalThis.fetch = mocks.mockFetch;
|
||||||
|
|
||||||
|
const deviceControlTool = addToolCalls.find(tool => tool.name === 'device_control');
|
||||||
|
expect(deviceControlTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!deviceControlTool) {
|
||||||
|
throw new Error('device_control tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await deviceControlTool.execute({
|
||||||
|
entity_id: 'light.living_room',
|
||||||
|
service: 'turn_on',
|
||||||
|
data: {
|
||||||
|
brightness: 255,
|
||||||
|
color_temp: 400
|
||||||
|
}
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.message).toBe('Successfully controlled device light.living_room');
|
||||||
|
|
||||||
|
// Verify the fetch call
|
||||||
|
type FetchArgs = [url: string, init: RequestInit];
|
||||||
|
const args = getMockCallArgs<FetchArgs>(mocks.mockFetch);
|
||||||
|
expect(args).toBeDefined();
|
||||||
|
|
||||||
|
if (!args) {
|
||||||
|
throw new Error('No fetch calls recorded');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [urlStr, options] = args;
|
||||||
|
expect(urlStr).toBe(`${TEST_CONFIG.HASS_HOST}/api/services/light/turn_on`);
|
||||||
|
expect(options).toEqual({
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
entity_id: 'light.living_room',
|
||||||
|
brightness: 255,
|
||||||
|
color_temp: 400
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle device control failure', async () => {
|
||||||
|
// Setup error response
|
||||||
|
mocks.mockFetch = mock(() => Promise.reject(new Error('Failed to control device')));
|
||||||
|
globalThis.fetch = mocks.mockFetch;
|
||||||
|
|
||||||
|
const deviceControlTool = addToolCalls.find(tool => tool.name === 'device_control');
|
||||||
|
expect(deviceControlTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!deviceControlTool) {
|
||||||
|
throw new Error('device_control tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await deviceControlTool.execute({
|
||||||
|
entity_id: 'light.living_room',
|
||||||
|
service: 'turn_on'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.message).toBe('Failed to control device: Failed to control device');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should require entity_id', async () => {
|
||||||
|
const deviceControlTool = addToolCalls.find(tool => tool.name === 'device_control');
|
||||||
|
expect(deviceControlTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!deviceControlTool) {
|
||||||
|
throw new Error('device_control tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await deviceControlTool.execute({
|
||||||
|
service: 'turn_on'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.message).toBe('Entity ID is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should require service', async () => {
|
||||||
|
const deviceControlTool = addToolCalls.find(tool => tool.name === 'device_control');
|
||||||
|
expect(deviceControlTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!deviceControlTool) {
|
||||||
|
throw new Error('device_control tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await deviceControlTool.execute({
|
||||||
|
entity_id: 'light.living_room'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.message).toBe('Service is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle invalid service domain', async () => {
|
||||||
|
const deviceControlTool = addToolCalls.find(tool => tool.name === 'device_control');
|
||||||
|
expect(deviceControlTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!deviceControlTool) {
|
||||||
|
throw new Error('device_control tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await deviceControlTool.execute({
|
||||||
|
entity_id: 'light.living_room',
|
||||||
|
service: 'invalid_domain.turn_on'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.message).toBe('Invalid service domain: invalid_domain');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
191
__tests__/tools/entity-state.test.ts
Normal file
191
__tests__/tools/entity-state.test.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
|
||||||
|
import {
|
||||||
|
type MockLiteMCPInstance,
|
||||||
|
type Tool,
|
||||||
|
type TestResponse,
|
||||||
|
TEST_CONFIG,
|
||||||
|
createMockLiteMCPInstance,
|
||||||
|
setupTestEnvironment,
|
||||||
|
cleanupMocks,
|
||||||
|
createMockResponse,
|
||||||
|
getMockCallArgs
|
||||||
|
} from '../utils/test-utils';
|
||||||
|
|
||||||
|
describe('Entity State Tools', () => {
|
||||||
|
let liteMcpInstance: MockLiteMCPInstance;
|
||||||
|
let addToolCalls: Tool[];
|
||||||
|
let mocks: ReturnType<typeof setupTestEnvironment>;
|
||||||
|
|
||||||
|
const mockEntityState = {
|
||||||
|
entity_id: 'light.living_room',
|
||||||
|
state: 'on',
|
||||||
|
attributes: {
|
||||||
|
brightness: 255,
|
||||||
|
color_temp: 400,
|
||||||
|
friendly_name: 'Living Room Light'
|
||||||
|
},
|
||||||
|
last_changed: '2024-03-20T12:00:00Z',
|
||||||
|
last_updated: '2024-03-20T12:00:00Z',
|
||||||
|
context: {
|
||||||
|
id: 'test_context_id',
|
||||||
|
parent_id: null,
|
||||||
|
user_id: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Setup test environment
|
||||||
|
mocks = setupTestEnvironment();
|
||||||
|
liteMcpInstance = createMockLiteMCPInstance();
|
||||||
|
|
||||||
|
// Import the module which will execute the main function
|
||||||
|
await import('../../src/index.js');
|
||||||
|
|
||||||
|
// Get the mock instance and tool calls
|
||||||
|
addToolCalls = liteMcpInstance.addTool.mock.calls.map(call => call.args[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupMocks({ liteMcpInstance, ...mocks });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('entity_state tool', () => {
|
||||||
|
test('should successfully get entity state', async () => {
|
||||||
|
// Setup response
|
||||||
|
mocks.mockFetch = mock(() => Promise.resolve(createMockResponse(mockEntityState)));
|
||||||
|
globalThis.fetch = mocks.mockFetch;
|
||||||
|
|
||||||
|
const entityStateTool = addToolCalls.find(tool => tool.name === 'entity_state');
|
||||||
|
expect(entityStateTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!entityStateTool) {
|
||||||
|
throw new Error('entity_state tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await entityStateTool.execute({
|
||||||
|
entity_id: 'light.living_room'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.state).toBe('on');
|
||||||
|
expect(result.attributes).toEqual(mockEntityState.attributes);
|
||||||
|
|
||||||
|
// Verify the fetch call
|
||||||
|
type FetchArgs = [url: string, init: RequestInit];
|
||||||
|
const args = getMockCallArgs<FetchArgs>(mocks.mockFetch);
|
||||||
|
expect(args).toBeDefined();
|
||||||
|
|
||||||
|
if (!args) {
|
||||||
|
throw new Error('No fetch calls recorded');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [urlStr, options] = args;
|
||||||
|
expect(urlStr).toBe(`${TEST_CONFIG.HASS_HOST}/api/states/light.living_room`);
|
||||||
|
expect(options).toEqual({
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle entity not found', async () => {
|
||||||
|
// Setup error response
|
||||||
|
mocks.mockFetch = mock(() => Promise.reject(new Error('Entity not found')));
|
||||||
|
globalThis.fetch = mocks.mockFetch;
|
||||||
|
|
||||||
|
const entityStateTool = addToolCalls.find(tool => tool.name === 'entity_state');
|
||||||
|
expect(entityStateTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!entityStateTool) {
|
||||||
|
throw new Error('entity_state tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await entityStateTool.execute({
|
||||||
|
entity_id: 'light.non_existent'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.message).toBe('Failed to get entity state: Entity not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should require entity_id', async () => {
|
||||||
|
const entityStateTool = addToolCalls.find(tool => tool.name === 'entity_state');
|
||||||
|
expect(entityStateTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!entityStateTool) {
|
||||||
|
throw new Error('entity_state tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await entityStateTool.execute({}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.message).toBe('Entity ID is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle invalid entity_id format', async () => {
|
||||||
|
const entityStateTool = addToolCalls.find(tool => tool.name === 'entity_state');
|
||||||
|
expect(entityStateTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!entityStateTool) {
|
||||||
|
throw new Error('entity_state tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await entityStateTool.execute({
|
||||||
|
entity_id: 'invalid_entity_id'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.message).toBe('Invalid entity ID format: invalid_entity_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should successfully get multiple entity states', async () => {
|
||||||
|
// Setup response
|
||||||
|
const mockStates = [
|
||||||
|
{ ...mockEntityState },
|
||||||
|
{
|
||||||
|
...mockEntityState,
|
||||||
|
entity_id: 'light.kitchen',
|
||||||
|
attributes: { ...mockEntityState.attributes, friendly_name: 'Kitchen Light' }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
mocks.mockFetch = mock(() => Promise.resolve(createMockResponse(mockStates)));
|
||||||
|
globalThis.fetch = mocks.mockFetch;
|
||||||
|
|
||||||
|
const entityStateTool = addToolCalls.find(tool => tool.name === 'entity_state');
|
||||||
|
expect(entityStateTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!entityStateTool) {
|
||||||
|
throw new Error('entity_state tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await entityStateTool.execute({
|
||||||
|
entity_id: ['light.living_room', 'light.kitchen']
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(Array.isArray(result.states)).toBe(true);
|
||||||
|
expect(result.states).toHaveLength(2);
|
||||||
|
expect(result.states[0].entity_id).toBe('light.living_room');
|
||||||
|
expect(result.states[1].entity_id).toBe('light.kitchen');
|
||||||
|
|
||||||
|
// Verify the fetch call
|
||||||
|
type FetchArgs = [url: string, init: RequestInit];
|
||||||
|
const args = getMockCallArgs<FetchArgs>(mocks.mockFetch);
|
||||||
|
expect(args).toBeDefined();
|
||||||
|
|
||||||
|
if (!args) {
|
||||||
|
throw new Error('No fetch calls recorded');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [urlStr, options] = args;
|
||||||
|
expect(urlStr).toBe(`${TEST_CONFIG.HASS_HOST}/api/states`);
|
||||||
|
expect(options).toEqual({
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
1
__tests__/tools/scene-control.test.ts
Normal file
1
__tests__/tools/scene-control.test.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
217
__tests__/tools/script-control.test.ts
Normal file
217
__tests__/tools/script-control.test.ts
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
|
||||||
|
import {
|
||||||
|
type MockLiteMCPInstance,
|
||||||
|
type Tool,
|
||||||
|
type TestResponse,
|
||||||
|
TEST_CONFIG,
|
||||||
|
createMockLiteMCPInstance,
|
||||||
|
setupTestEnvironment,
|
||||||
|
cleanupMocks,
|
||||||
|
createMockResponse,
|
||||||
|
getMockCallArgs
|
||||||
|
} from '../utils/test-utils';
|
||||||
|
|
||||||
|
describe('Script Control Tools', () => {
|
||||||
|
let liteMcpInstance: MockLiteMCPInstance;
|
||||||
|
let addToolCalls: Tool[];
|
||||||
|
let mocks: ReturnType<typeof setupTestEnvironment>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Setup test environment
|
||||||
|
mocks = setupTestEnvironment();
|
||||||
|
liteMcpInstance = createMockLiteMCPInstance();
|
||||||
|
|
||||||
|
// Import the module which will execute the main function
|
||||||
|
await import('../../src/index.js');
|
||||||
|
|
||||||
|
// Get the mock instance and tool calls
|
||||||
|
addToolCalls = liteMcpInstance.addTool.mock.calls.map(call => call.args[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanupMocks({ liteMcpInstance, ...mocks });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('script_control tool', () => {
|
||||||
|
test('should successfully execute a script', async () => {
|
||||||
|
// Setup response
|
||||||
|
mocks.mockFetch = mock(() => Promise.resolve(createMockResponse({ success: true })));
|
||||||
|
globalThis.fetch = mocks.mockFetch;
|
||||||
|
|
||||||
|
const scriptControlTool = addToolCalls.find(tool => tool.name === 'script_control');
|
||||||
|
expect(scriptControlTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!scriptControlTool) {
|
||||||
|
throw new Error('script_control tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await scriptControlTool.execute({
|
||||||
|
script_id: 'script.welcome_home',
|
||||||
|
action: 'start',
|
||||||
|
variables: {
|
||||||
|
brightness: 100,
|
||||||
|
color_temp: 300
|
||||||
|
}
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.message).toBe('Successfully executed script script.welcome_home');
|
||||||
|
|
||||||
|
// Verify the fetch call
|
||||||
|
type FetchArgs = [url: string, init: RequestInit];
|
||||||
|
const args = getMockCallArgs<FetchArgs>(mocks.mockFetch);
|
||||||
|
expect(args).toBeDefined();
|
||||||
|
|
||||||
|
if (!args) {
|
||||||
|
throw new Error('No fetch calls recorded');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [urlStr, options] = args;
|
||||||
|
expect(urlStr).toBe(`${TEST_CONFIG.HASS_HOST}/api/services/script/turn_on`);
|
||||||
|
expect(options).toEqual({
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
entity_id: 'script.welcome_home',
|
||||||
|
variables: {
|
||||||
|
brightness: 100,
|
||||||
|
color_temp: 300
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should successfully stop a script', async () => {
|
||||||
|
// Setup response
|
||||||
|
mocks.mockFetch = mock(() => Promise.resolve(createMockResponse({ success: true })));
|
||||||
|
globalThis.fetch = mocks.mockFetch;
|
||||||
|
|
||||||
|
const scriptControlTool = addToolCalls.find(tool => tool.name === 'script_control');
|
||||||
|
expect(scriptControlTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!scriptControlTool) {
|
||||||
|
throw new Error('script_control tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await scriptControlTool.execute({
|
||||||
|
script_id: 'script.welcome_home',
|
||||||
|
action: 'stop'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.message).toBe('Successfully stopped script script.welcome_home');
|
||||||
|
|
||||||
|
// Verify the fetch call
|
||||||
|
type FetchArgs = [url: string, init: RequestInit];
|
||||||
|
const args = getMockCallArgs<FetchArgs>(mocks.mockFetch);
|
||||||
|
expect(args).toBeDefined();
|
||||||
|
|
||||||
|
if (!args) {
|
||||||
|
throw new Error('No fetch calls recorded');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [urlStr, options] = args;
|
||||||
|
expect(urlStr).toBe(`${TEST_CONFIG.HASS_HOST}/api/services/script/turn_off`);
|
||||||
|
expect(options).toEqual({
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
entity_id: 'script.welcome_home'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle script execution failure', async () => {
|
||||||
|
// Setup error response
|
||||||
|
mocks.mockFetch = mock(() => Promise.reject(new Error('Failed to execute script')));
|
||||||
|
globalThis.fetch = mocks.mockFetch;
|
||||||
|
|
||||||
|
const scriptControlTool = addToolCalls.find(tool => tool.name === 'script_control');
|
||||||
|
expect(scriptControlTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!scriptControlTool) {
|
||||||
|
throw new Error('script_control tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await scriptControlTool.execute({
|
||||||
|
script_id: 'script.welcome_home',
|
||||||
|
action: 'start'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.message).toBe('Failed to execute script: Failed to execute script');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should require script_id', async () => {
|
||||||
|
const scriptControlTool = addToolCalls.find(tool => tool.name === 'script_control');
|
||||||
|
expect(scriptControlTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!scriptControlTool) {
|
||||||
|
throw new Error('script_control tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await scriptControlTool.execute({
|
||||||
|
action: 'start'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.message).toBe('Script ID is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should require action', async () => {
|
||||||
|
const scriptControlTool = addToolCalls.find(tool => tool.name === 'script_control');
|
||||||
|
expect(scriptControlTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!scriptControlTool) {
|
||||||
|
throw new Error('script_control tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await scriptControlTool.execute({
|
||||||
|
script_id: 'script.welcome_home'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.message).toBe('Action is required');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle invalid script_id format', async () => {
|
||||||
|
const scriptControlTool = addToolCalls.find(tool => tool.name === 'script_control');
|
||||||
|
expect(scriptControlTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!scriptControlTool) {
|
||||||
|
throw new Error('script_control tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await scriptControlTool.execute({
|
||||||
|
script_id: 'invalid_script_id',
|
||||||
|
action: 'start'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.message).toBe('Invalid script ID format: invalid_script_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle invalid action', async () => {
|
||||||
|
const scriptControlTool = addToolCalls.find(tool => tool.name === 'script_control');
|
||||||
|
expect(scriptControlTool).toBeDefined();
|
||||||
|
|
||||||
|
if (!scriptControlTool) {
|
||||||
|
throw new Error('script_control tool not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await scriptControlTool.execute({
|
||||||
|
script_id: 'script.welcome_home',
|
||||||
|
action: 'invalid_action'
|
||||||
|
}) as TestResponse;
|
||||||
|
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.message).toBe('Invalid action: invalid_action');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
19
__tests__/types/litemcp.d.ts
vendored
Normal file
19
__tests__/types/litemcp.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
declare module 'litemcp' {
|
||||||
|
export interface Tool {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
parameters: Record<string, unknown>;
|
||||||
|
execute: (params: Record<string, unknown>) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LiteMCPOptions {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LiteMCP {
|
||||||
|
constructor(options: LiteMCPOptions);
|
||||||
|
addTool(tool: Tool): void;
|
||||||
|
start(): Promise<void>;
|
||||||
|
}
|
||||||
|
}
|
||||||
148
__tests__/utils/test-utils.ts
Normal file
148
__tests__/utils/test-utils.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { mock } from "bun:test";
|
||||||
|
import type { Mock } from "bun:test";
|
||||||
|
import type { WebSocket } from 'ws';
|
||||||
|
|
||||||
|
// Common Types
|
||||||
|
export interface Tool {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
parameters: Record<string, unknown>;
|
||||||
|
execute: (params: Record<string, unknown>) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MockLiteMCPInstance {
|
||||||
|
addTool: Mock<(tool: Tool) => void>;
|
||||||
|
start: Mock<() => Promise<void>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MockServices {
|
||||||
|
light: {
|
||||||
|
turn_on: Mock<() => Promise<{ success: boolean }>>;
|
||||||
|
turn_off: Mock<() => Promise<{ success: boolean }>>;
|
||||||
|
};
|
||||||
|
climate: {
|
||||||
|
set_temperature: Mock<() => Promise<{ success: boolean }>>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MockHassInstance {
|
||||||
|
services: MockServices;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TestResponse = {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
automation_id?: string;
|
||||||
|
new_automation_id?: string;
|
||||||
|
state?: string;
|
||||||
|
attributes?: Record<string, any>;
|
||||||
|
states?: Array<{
|
||||||
|
entity_id: string;
|
||||||
|
state: string;
|
||||||
|
attributes: Record<string, any>;
|
||||||
|
last_changed: string;
|
||||||
|
last_updated: string;
|
||||||
|
context: {
|
||||||
|
id: string;
|
||||||
|
parent_id: string | null;
|
||||||
|
user_id: string | null;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test Configuration
|
||||||
|
export const TEST_CONFIG = {
|
||||||
|
HASS_HOST: process.env.TEST_HASS_HOST || 'http://localhost:8123',
|
||||||
|
HASS_TOKEN: process.env.TEST_HASS_TOKEN || 'test_token',
|
||||||
|
HASS_SOCKET_URL: process.env.TEST_HASS_SOCKET_URL || 'ws://localhost:8123/api/websocket'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Mock WebSocket Implementation
|
||||||
|
export class MockWebSocket {
|
||||||
|
public static readonly CONNECTING = 0;
|
||||||
|
public static readonly OPEN = 1;
|
||||||
|
public static readonly CLOSING = 2;
|
||||||
|
public static readonly CLOSED = 3;
|
||||||
|
|
||||||
|
public readyState: 0 | 1 | 2 | 3 = MockWebSocket.OPEN;
|
||||||
|
public bufferedAmount = 0;
|
||||||
|
public extensions = '';
|
||||||
|
public protocol = '';
|
||||||
|
public url = '';
|
||||||
|
public binaryType: 'arraybuffer' | 'nodebuffer' | 'fragments' = 'arraybuffer';
|
||||||
|
|
||||||
|
public onopen: ((event: any) => void) | null = null;
|
||||||
|
public onerror: ((event: any) => void) | null = null;
|
||||||
|
public onclose: ((event: any) => void) | null = null;
|
||||||
|
public onmessage: ((event: any) => void) | null = null;
|
||||||
|
|
||||||
|
public addEventListener = mock(() => undefined);
|
||||||
|
public removeEventListener = mock(() => undefined);
|
||||||
|
public send = mock(() => undefined);
|
||||||
|
public close = mock(() => undefined);
|
||||||
|
public ping = mock(() => undefined);
|
||||||
|
public pong = mock(() => undefined);
|
||||||
|
public terminate = mock(() => undefined);
|
||||||
|
|
||||||
|
constructor(url: string | URL, protocols?: string | string[]) {
|
||||||
|
this.url = url.toString();
|
||||||
|
if (protocols) {
|
||||||
|
this.protocol = Array.isArray(protocols) ? protocols[0] : protocols;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock Service Instances
|
||||||
|
export const createMockServices = (): MockServices => ({
|
||||||
|
light: {
|
||||||
|
turn_on: mock(() => Promise.resolve({ success: true })),
|
||||||
|
turn_off: mock(() => Promise.resolve({ success: true }))
|
||||||
|
},
|
||||||
|
climate: {
|
||||||
|
set_temperature: mock(() => Promise.resolve({ success: true }))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createMockLiteMCPInstance = (): MockLiteMCPInstance => ({
|
||||||
|
addTool: mock((tool: Tool) => undefined),
|
||||||
|
start: mock(() => Promise.resolve())
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper Functions
|
||||||
|
export const createMockResponse = <T>(data: T, status = 200): Response => {
|
||||||
|
return new Response(JSON.stringify(data), { status });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMockCallArgs = <T extends unknown[]>(
|
||||||
|
mock: Mock<(...args: any[]) => any>,
|
||||||
|
callIndex = 0
|
||||||
|
): T | undefined => {
|
||||||
|
const call = mock.mock.calls[callIndex];
|
||||||
|
return call?.args as T | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setupTestEnvironment = () => {
|
||||||
|
// Setup test environment variables
|
||||||
|
Object.entries(TEST_CONFIG).forEach(([key, value]) => {
|
||||||
|
process.env[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create fetch mock
|
||||||
|
const mockFetch = mock(() => Promise.resolve(createMockResponse({ state: 'connected' })));
|
||||||
|
|
||||||
|
// Override globals
|
||||||
|
globalThis.fetch = mockFetch;
|
||||||
|
globalThis.WebSocket = MockWebSocket as any;
|
||||||
|
|
||||||
|
return { mockFetch };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cleanupMocks = (mocks: {
|
||||||
|
liteMcpInstance: MockLiteMCPInstance;
|
||||||
|
mockFetch: Mock<() => Promise<Response>>;
|
||||||
|
}) => {
|
||||||
|
mocks.liteMcpInstance.addTool.mock.calls = [];
|
||||||
|
mocks.liteMcpInstance.start.mock.calls = [];
|
||||||
|
mocks.mockFetch = mock(() => Promise.resolve(new Response()));
|
||||||
|
globalThis.fetch = mocks.mockFetch;
|
||||||
|
};
|
||||||
36
bun.lock
Executable file → Normal file
36
bun.lock
Executable file → Normal file
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 0,
|
"lockfileVersion": 1,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -9,11 +9,13 @@
|
|||||||
"@types/node": "^20.11.24",
|
"@types/node": "^20.11.24",
|
||||||
"@types/sanitize-html": "^2.9.5",
|
"@types/sanitize-html": "^2.9.5",
|
||||||
"@types/ws": "^8.5.10",
|
"@types/ws": "^8.5.10",
|
||||||
|
"@xmldom/xmldom": "^0.9.7",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"elysia": "^1.2.11",
|
"elysia": "^1.2.11",
|
||||||
"helmet": "^7.1.0",
|
"helmet": "^7.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
|
"openai": "^4.82.0",
|
||||||
"sanitize-html": "^2.11.0",
|
"sanitize-html": "^2.11.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
@@ -81,6 +83,8 @@
|
|||||||
|
|
||||||
"@types/node": ["@types/node@20.17.17", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-/WndGO4kIfMicEQLTi/mDANUu/iVUhT7KboZPdEqqHQ4aTS+3qT3U5gIqWDFV+XouorjfgGqvKILJeHhuQgFYg=="],
|
"@types/node": ["@types/node@20.17.17", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-/WndGO4kIfMicEQLTi/mDANUu/iVUhT7KboZPdEqqHQ4aTS+3qT3U5gIqWDFV+XouorjfgGqvKILJeHhuQgFYg=="],
|
||||||
|
|
||||||
|
"@types/node-fetch": ["@types/node-fetch@2.6.12", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="],
|
||||||
|
|
||||||
"@types/sanitize-html": ["@types/sanitize-html@2.13.0", "", { "dependencies": { "htmlparser2": "^8.0.0" } }, "sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ=="],
|
"@types/sanitize-html": ["@types/sanitize-html@2.13.0", "", { "dependencies": { "htmlparser2": "^8.0.0" } }, "sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ=="],
|
||||||
|
|
||||||
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
|
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
|
||||||
@@ -109,10 +113,16 @@
|
|||||||
|
|
||||||
"@unhead/schema": ["@unhead/schema@1.11.18", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-a3TA/OJCRdfbFhcA3Hq24k1ZU1o9szicESrw8DZcGyQFacHnh84mVgnyqSkMnwgCmfN4kvjSiTBlLEHS6+wATw=="],
|
"@unhead/schema": ["@unhead/schema@1.11.18", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-a3TA/OJCRdfbFhcA3Hq24k1ZU1o9szicESrw8DZcGyQFacHnh84mVgnyqSkMnwgCmfN4kvjSiTBlLEHS6+wATw=="],
|
||||||
|
|
||||||
|
"@xmldom/xmldom": ["@xmldom/xmldom@0.9.7", "", {}, "sha512-syvR8iIJjpTZ/stv7l89UAViwGFh6lbheeOaqSxkYx9YNmIVvPTRH+CT/fpykFtUx5N+8eSMDRvggF9J8GEPzQ=="],
|
||||||
|
|
||||||
|
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
|
"acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
|
||||||
|
|
||||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||||
|
|
||||||
|
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
|
||||||
|
|
||||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||||
|
|
||||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
@@ -233,6 +243,8 @@
|
|||||||
|
|
||||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||||
|
|
||||||
|
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
|
||||||
|
|
||||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
"fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="],
|
"fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="],
|
||||||
@@ -267,6 +279,10 @@
|
|||||||
|
|
||||||
"form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="],
|
"form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="],
|
||||||
|
|
||||||
|
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
|
||||||
|
|
||||||
|
"formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
|
||||||
|
|
||||||
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
||||||
|
|
||||||
"formidable": ["formidable@2.1.2", "", { "dependencies": { "dezalgo": "^1.0.4", "hexoid": "^1.0.0", "once": "^1.4.0", "qs": "^6.11.0" } }, "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g=="],
|
"formidable": ["formidable@2.1.2", "", { "dependencies": { "dezalgo": "^1.0.4", "hexoid": "^1.0.0", "once": "^1.4.0", "qs": "^6.11.0" } }, "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g=="],
|
||||||
@@ -305,6 +321,8 @@
|
|||||||
|
|
||||||
"htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="],
|
"htmlparser2": ["htmlparser2@8.0.2", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1", "entities": "^4.4.0" } }, "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA=="],
|
||||||
|
|
||||||
|
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
|
||||||
|
|
||||||
"husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
|
"husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
|
||||||
|
|
||||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
@@ -411,6 +429,8 @@
|
|||||||
|
|
||||||
"one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="],
|
"one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="],
|
||||||
|
|
||||||
|
"openai": ["openai@4.82.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-1bTxOVGZuVGsKKUWbh3BEwX1QxIXUftJv+9COhhGGVDTFwiaOd4gWsMynF2ewj1mg6by3/O+U8+EEHpWRdPaJg=="],
|
||||||
|
|
||||||
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
"openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
|
||||||
|
|
||||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||||
@@ -509,6 +529,8 @@
|
|||||||
|
|
||||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||||
|
|
||||||
|
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||||
|
|
||||||
"triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="],
|
"triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="],
|
||||||
|
|
||||||
"ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw=="],
|
"ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw=="],
|
||||||
@@ -531,6 +553,10 @@
|
|||||||
|
|
||||||
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
|
||||||
|
|
||||||
|
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||||
|
|
||||||
|
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||||
|
|
||||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
"winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="],
|
"winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="],
|
||||||
@@ -561,10 +587,18 @@
|
|||||||
|
|
||||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
|
"formdata-node/web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
|
||||||
|
|
||||||
|
"openai/@types/node": ["@types/node@18.19.75", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-UIksWtThob6ZVSyxcOqCLOUNg/dyO1Qvx4McgeuhrEtHTLFTf7BBhEazaE4K806FGTPtzd/2sE90qn4fVr7cyw=="],
|
||||||
|
|
||||||
|
"openai/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||||
|
|
||||||
"@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.7", "", {}, "sha512-oOTG3JQifg55U3DhKB7WdNIxFnJzbPJe7rqdyWdio977l8IkxQTVmObftJhdNIMvhV2K+1f/bDoMQGu6yTaD0A=="],
|
"@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.7", "", {}, "sha512-oOTG3JQifg55U3DhKB7WdNIxFnJzbPJe7rqdyWdio977l8IkxQTVmObftJhdNIMvhV2K+1f/bDoMQGu6yTaD0A=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||||
|
|
||||||
"color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
"color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
||||||
|
|
||||||
|
"openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,36 +33,35 @@ RUN apt-get update && apt-get install -y \
|
|||||||
libasound2 \
|
libasound2 \
|
||||||
libasound2-plugins \
|
libasound2-plugins \
|
||||||
pulseaudio \
|
pulseaudio \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
pulseaudio-utils \
|
||||||
|
libpulse0 \
|
||||||
|
libportaudio2 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& mkdir -p /var/run/pulse /var/lib/pulse
|
||||||
|
|
||||||
# Create necessary directories
|
# Create necessary directories
|
||||||
RUN mkdir -p /models/wake_word /audio
|
RUN mkdir -p /models/wake_word /audio && \
|
||||||
|
chown -R 1000:1000 /models /audio && \
|
||||||
|
mkdir -p /home/user/.config/pulse && \
|
||||||
|
chown -R 1000:1000 /home/user
|
||||||
|
|
||||||
# Set working directory
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy the wake word detection script
|
# Copy the wake word detection script and audio setup script
|
||||||
COPY wake_word_detector.py .
|
COPY wake_word_detector.py .
|
||||||
|
COPY setup-audio.sh /setup-audio.sh
|
||||||
|
RUN chmod +x /setup-audio.sh
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
ENV WHISPER_MODEL_PATH=/models \
|
ENV WHISPER_MODEL_PATH=/models \
|
||||||
WAKEWORD_MODEL_PATH=/models/wake_word \
|
WAKEWORD_MODEL_PATH=/models/wake_word \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
ASR_MODEL=base.en \
|
PULSE_SERVER=unix:/run/user/1000/pulse/native \
|
||||||
ASR_MODEL_PATH=/models
|
HOME=/home/user
|
||||||
|
|
||||||
# Add resource limits to Python
|
# Run as the host user
|
||||||
ENV PYTHONMALLOC=malloc \
|
USER 1000:1000
|
||||||
MALLOC_TRIM_THRESHOLD_=100000 \
|
|
||||||
PYTHONDEVMODE=1
|
|
||||||
|
|
||||||
# Add healthcheck
|
# Start the application
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
CMD ["/setup-audio.sh"]
|
||||||
CMD ps aux | grep '[p]ython' || exit 1
|
|
||||||
|
|
||||||
# Copy audio setup script
|
|
||||||
COPY setup-audio.sh /setup-audio.sh
|
|
||||||
RUN chmod +x /setup-audio.sh
|
|
||||||
|
|
||||||
# Start command
|
|
||||||
CMD ["/bin/bash", "-c", "/setup-audio.sh && python -u wake_word_detector.py"]
|
|
||||||
@@ -1,7 +1,25 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Wait for PulseAudio to be ready
|
# Wait for PulseAudio socket to be available
|
||||||
sleep 2
|
while [ ! -e /run/user/1000/pulse/native ]; do
|
||||||
|
echo "Waiting for PulseAudio socket..."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Test PulseAudio connection
|
||||||
|
pactl info || {
|
||||||
|
echo "Failed to connect to PulseAudio server"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# List audio devices
|
||||||
|
pactl list sources || {
|
||||||
|
echo "Failed to list audio devices"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start the wake word detector
|
||||||
|
python /app/wake_word_detector.py
|
||||||
|
|
||||||
# Mute the monitor to prevent feedback
|
# Mute the monitor to prevent feedback
|
||||||
pactl set-source-mute alsa_output.pci-0000_00_1b.0.analog-stereo.monitor 1
|
pactl set-source-mute alsa_output.pci-0000_00_1b.0.analog-stereo.monitor 1
|
||||||
|
|||||||
310
docs/development/best-practices.md
Normal file
310
docs/development/best-practices.md
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
# Development Best Practices
|
||||||
|
|
||||||
|
This guide outlines the best practices for developing tools and features for the Home Assistant MCP.
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
|
||||||
|
1. Use TypeScript for all new code
|
||||||
|
2. Enable strict mode
|
||||||
|
3. Use explicit types
|
||||||
|
4. Avoid `any` type
|
||||||
|
5. Use interfaces over types
|
||||||
|
6. Document with JSDoc comments
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* Represents a device in the system.
|
||||||
|
* @interface
|
||||||
|
*/
|
||||||
|
interface Device {
|
||||||
|
/** Unique device identifier */
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/** Human-readable device name */
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/** Device state */
|
||||||
|
state: DeviceState;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
|
||||||
|
1. Use PascalCase for:
|
||||||
|
- Classes
|
||||||
|
- Interfaces
|
||||||
|
- Types
|
||||||
|
- Enums
|
||||||
|
|
||||||
|
2. Use camelCase for:
|
||||||
|
- Variables
|
||||||
|
- Functions
|
||||||
|
- Methods
|
||||||
|
- Properties
|
||||||
|
|
||||||
|
3. Use UPPER_SNAKE_CASE for:
|
||||||
|
- Constants
|
||||||
|
- Enum values
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class DeviceManager {
|
||||||
|
private readonly DEFAULT_TIMEOUT = 5000;
|
||||||
|
|
||||||
|
async getDeviceState(deviceId: string): Promise<DeviceState> {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### SOLID Principles
|
||||||
|
|
||||||
|
1. Single Responsibility
|
||||||
|
- Each class/module has one job
|
||||||
|
- Split complex functionality
|
||||||
|
|
||||||
|
2. Open/Closed
|
||||||
|
- Open for extension
|
||||||
|
- Closed for modification
|
||||||
|
|
||||||
|
3. Liskov Substitution
|
||||||
|
- Subtypes must be substitutable
|
||||||
|
- Use interfaces properly
|
||||||
|
|
||||||
|
4. Interface Segregation
|
||||||
|
- Keep interfaces focused
|
||||||
|
- Split large interfaces
|
||||||
|
|
||||||
|
5. Dependency Inversion
|
||||||
|
- Depend on abstractions
|
||||||
|
- Use dependency injection
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Bad
|
||||||
|
class DeviceManager {
|
||||||
|
async getState() { /* ... */ }
|
||||||
|
async setState() { /* ... */ }
|
||||||
|
async sendNotification() { /* ... */ } // Wrong responsibility
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good
|
||||||
|
class DeviceManager {
|
||||||
|
constructor(
|
||||||
|
private notifier: NotificationService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getState() { /* ... */ }
|
||||||
|
async setState() { /* ... */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationService {
|
||||||
|
async send() { /* ... */ }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
1. Use custom error classes
|
||||||
|
2. Include error codes
|
||||||
|
3. Provide meaningful messages
|
||||||
|
4. Include error context
|
||||||
|
5. Handle async errors
|
||||||
|
6. Log appropriately
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class DeviceError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public code: string,
|
||||||
|
public context: Record<string, any>
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'DeviceError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await device.connect();
|
||||||
|
} catch (error) {
|
||||||
|
throw new DeviceError(
|
||||||
|
'Failed to connect to device',
|
||||||
|
'DEVICE_CONNECTION_ERROR',
|
||||||
|
{ deviceId: device.id, attempt: 1 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Guidelines
|
||||||
|
|
||||||
|
1. Write unit tests first
|
||||||
|
2. Use meaningful descriptions
|
||||||
|
3. Test edge cases
|
||||||
|
4. Mock external dependencies
|
||||||
|
5. Keep tests focused
|
||||||
|
6. Use test fixtures
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('DeviceManager', () => {
|
||||||
|
let manager: DeviceManager;
|
||||||
|
let mockDevice: jest.Mocked<Device>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockDevice = {
|
||||||
|
id: 'test_device',
|
||||||
|
getState: jest.fn()
|
||||||
|
};
|
||||||
|
manager = new DeviceManager(mockDevice);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get device state', async () => {
|
||||||
|
mockDevice.getState.mockResolvedValue('on');
|
||||||
|
const state = await manager.getDeviceState();
|
||||||
|
expect(state).toBe('on');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
### Optimization
|
||||||
|
|
||||||
|
1. Use caching
|
||||||
|
2. Implement pagination
|
||||||
|
3. Optimize database queries
|
||||||
|
4. Use connection pooling
|
||||||
|
5. Implement rate limiting
|
||||||
|
6. Batch operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class DeviceCache {
|
||||||
|
private cache = new Map<string, CacheEntry>();
|
||||||
|
private readonly TTL = 60000; // 1 minute
|
||||||
|
|
||||||
|
async getDevice(id: string): Promise<Device> {
|
||||||
|
const cached = this.cache.get(id);
|
||||||
|
if (cached && Date.now() - cached.timestamp < this.TTL) {
|
||||||
|
return cached.device;
|
||||||
|
}
|
||||||
|
|
||||||
|
const device = await this.fetchDevice(id);
|
||||||
|
this.cache.set(id, {
|
||||||
|
device,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
### Guidelines
|
||||||
|
|
||||||
|
1. Validate all input
|
||||||
|
2. Use parameterized queries
|
||||||
|
3. Implement rate limiting
|
||||||
|
4. Use proper authentication
|
||||||
|
5. Follow OWASP guidelines
|
||||||
|
6. Sanitize output
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class InputValidator {
|
||||||
|
static validateDeviceId(id: string): boolean {
|
||||||
|
return /^[a-zA-Z0-9_-]{1,64}$/.test(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static sanitizeOutput(data: any): any {
|
||||||
|
// Implement output sanitization
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
### Standards
|
||||||
|
|
||||||
|
1. Use JSDoc comments
|
||||||
|
2. Document interfaces
|
||||||
|
3. Include examples
|
||||||
|
4. Document errors
|
||||||
|
5. Keep docs updated
|
||||||
|
6. Use markdown
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* Manages device operations.
|
||||||
|
* @class
|
||||||
|
*/
|
||||||
|
class DeviceManager {
|
||||||
|
/**
|
||||||
|
* Gets the current state of a device.
|
||||||
|
* @param {string} deviceId - The device identifier.
|
||||||
|
* @returns {Promise<DeviceState>} The current device state.
|
||||||
|
* @throws {DeviceError} If device is not found or unavailable.
|
||||||
|
* @example
|
||||||
|
* const state = await deviceManager.getDeviceState('living_room_light');
|
||||||
|
*/
|
||||||
|
async getDeviceState(deviceId: string): Promise<DeviceState> {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
1. Use appropriate levels
|
||||||
|
2. Include context
|
||||||
|
3. Structure log data
|
||||||
|
4. Handle sensitive data
|
||||||
|
5. Implement rotation
|
||||||
|
6. Use correlation IDs
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class Logger {
|
||||||
|
info(message: string, context: Record<string, any>) {
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
level: 'info',
|
||||||
|
message,
|
||||||
|
context,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
correlationId: context.correlationId
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version Control
|
||||||
|
|
||||||
|
### Guidelines
|
||||||
|
|
||||||
|
1. Use meaningful commits
|
||||||
|
2. Follow branching strategy
|
||||||
|
3. Write good PR descriptions
|
||||||
|
4. Review code thoroughly
|
||||||
|
5. Keep changes focused
|
||||||
|
6. Use conventional commits
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Good commit messages
|
||||||
|
git commit -m "feat(device): add support for zigbee devices"
|
||||||
|
git commit -m "fix(api): handle timeout errors properly"
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Tool Development Guide](tools.md)
|
||||||
|
- [Interface Documentation](interfaces.md)
|
||||||
|
- [Testing Guide](../testing.md)
|
||||||
296
docs/development/interfaces.md
Normal file
296
docs/development/interfaces.md
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
# Interface Documentation
|
||||||
|
|
||||||
|
This document describes the core interfaces used throughout the Home Assistant MCP.
|
||||||
|
|
||||||
|
## Core Interfaces
|
||||||
|
|
||||||
|
### Tool Interface
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Tool {
|
||||||
|
/** Unique identifier for the tool */
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/** Human-readable name */
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/** Detailed description */
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
/** Semantic version */
|
||||||
|
version: string;
|
||||||
|
|
||||||
|
/** Tool category */
|
||||||
|
category: ToolCategory;
|
||||||
|
|
||||||
|
/** Execute tool functionality */
|
||||||
|
execute(params: any): Promise<ToolResult>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tool Result
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ToolResult {
|
||||||
|
/** Operation success status */
|
||||||
|
success: boolean;
|
||||||
|
|
||||||
|
/** Response data */
|
||||||
|
data?: any;
|
||||||
|
|
||||||
|
/** Error message if failed */
|
||||||
|
message?: string;
|
||||||
|
|
||||||
|
/** Error code if failed */
|
||||||
|
error_code?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tool Category
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
enum ToolCategory {
|
||||||
|
DeviceManagement = 'device_management',
|
||||||
|
HistoryState = 'history_state',
|
||||||
|
Automation = 'automation',
|
||||||
|
AddonsPackages = 'addons_packages',
|
||||||
|
Notifications = 'notifications',
|
||||||
|
Events = 'events',
|
||||||
|
Utility = 'utility'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Interfaces
|
||||||
|
|
||||||
|
### Event Subscription
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface EventSubscription {
|
||||||
|
/** Unique subscription ID */
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/** Event type to subscribe to */
|
||||||
|
event_type: string;
|
||||||
|
|
||||||
|
/** Optional entity ID filter */
|
||||||
|
entity_id?: string;
|
||||||
|
|
||||||
|
/** Optional domain filter */
|
||||||
|
domain?: string;
|
||||||
|
|
||||||
|
/** Subscription creation timestamp */
|
||||||
|
created_at: string;
|
||||||
|
|
||||||
|
/** Last event timestamp */
|
||||||
|
last_event?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Message
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface EventMessage {
|
||||||
|
/** Event type */
|
||||||
|
event_type: string;
|
||||||
|
|
||||||
|
/** Entity ID if applicable */
|
||||||
|
entity_id?: string;
|
||||||
|
|
||||||
|
/** Event data */
|
||||||
|
data: any;
|
||||||
|
|
||||||
|
/** Event origin */
|
||||||
|
origin: 'LOCAL' | 'REMOTE';
|
||||||
|
|
||||||
|
/** Event timestamp */
|
||||||
|
time_fired: string;
|
||||||
|
|
||||||
|
/** Event context */
|
||||||
|
context: EventContext;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Device Interfaces
|
||||||
|
|
||||||
|
### Device
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Device {
|
||||||
|
/** Device ID */
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/** Device name */
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/** Device domain */
|
||||||
|
domain: string;
|
||||||
|
|
||||||
|
/** Current state */
|
||||||
|
state: string;
|
||||||
|
|
||||||
|
/** Device attributes */
|
||||||
|
attributes: Record<string, any>;
|
||||||
|
|
||||||
|
/** Device capabilities */
|
||||||
|
capabilities: DeviceCapabilities;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Device Capabilities
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface DeviceCapabilities {
|
||||||
|
/** Supported features */
|
||||||
|
features: string[];
|
||||||
|
|
||||||
|
/** Supported commands */
|
||||||
|
commands: string[];
|
||||||
|
|
||||||
|
/** State attributes */
|
||||||
|
attributes: {
|
||||||
|
/** Attribute name */
|
||||||
|
[key: string]: {
|
||||||
|
/** Attribute type */
|
||||||
|
type: 'string' | 'number' | 'boolean' | 'object';
|
||||||
|
/** Attribute description */
|
||||||
|
description: string;
|
||||||
|
/** Optional value constraints */
|
||||||
|
constraints?: {
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
enum?: any[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication Interfaces
|
||||||
|
|
||||||
|
### Auth Token
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface AuthToken {
|
||||||
|
/** Token value */
|
||||||
|
token: string;
|
||||||
|
|
||||||
|
/** Token type */
|
||||||
|
type: 'bearer' | 'jwt';
|
||||||
|
|
||||||
|
/** Expiration timestamp */
|
||||||
|
expires_at: string;
|
||||||
|
|
||||||
|
/** Token refresh info */
|
||||||
|
refresh?: {
|
||||||
|
token: string;
|
||||||
|
expires_at: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### User
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface User {
|
||||||
|
/** User ID */
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/** Username */
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
/** User type */
|
||||||
|
type: 'admin' | 'user' | 'service';
|
||||||
|
|
||||||
|
/** User permissions */
|
||||||
|
permissions: string[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Interfaces
|
||||||
|
|
||||||
|
### Tool Error
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ToolError extends Error {
|
||||||
|
/** Error code */
|
||||||
|
code: string;
|
||||||
|
|
||||||
|
/** HTTP status code */
|
||||||
|
status: number;
|
||||||
|
|
||||||
|
/** Error details */
|
||||||
|
details?: Record<string, any>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validation Error
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ValidationError {
|
||||||
|
/** Error path */
|
||||||
|
path: string;
|
||||||
|
|
||||||
|
/** Error message */
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
/** Error code */
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Interfaces
|
||||||
|
|
||||||
|
### Tool Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ToolConfig {
|
||||||
|
/** Enable/disable tool */
|
||||||
|
enabled: boolean;
|
||||||
|
|
||||||
|
/** Tool-specific settings */
|
||||||
|
settings: Record<string, any>;
|
||||||
|
|
||||||
|
/** Rate limiting */
|
||||||
|
rate_limit?: {
|
||||||
|
/** Max requests */
|
||||||
|
max: number;
|
||||||
|
/** Time window in seconds */
|
||||||
|
window: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### System Configuration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface SystemConfig {
|
||||||
|
/** System name */
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/** Environment */
|
||||||
|
environment: 'development' | 'production';
|
||||||
|
|
||||||
|
/** Log level */
|
||||||
|
log_level: 'debug' | 'info' | 'warn' | 'error';
|
||||||
|
|
||||||
|
/** Tool configurations */
|
||||||
|
tools: Record<string, ToolConfig>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. Use TypeScript for all interfaces
|
||||||
|
2. Include JSDoc comments
|
||||||
|
3. Use strict typing
|
||||||
|
4. Keep interfaces focused
|
||||||
|
5. Use consistent naming
|
||||||
|
6. Document constraints
|
||||||
|
7. Version interfaces
|
||||||
|
8. Include examples
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Tool Development Guide](tools.md)
|
||||||
|
- [Best Practices](best-practices.md)
|
||||||
|
- [Testing Guide](../testing.md)
|
||||||
323
docs/development/test-migration-guide.md
Normal file
323
docs/development/test-migration-guide.md
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
# Migrating Tests from Jest to Bun
|
||||||
|
|
||||||
|
This guide provides instructions for migrating test files from Jest to Bun's test framework.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
- [Basic Setup](#basic-setup)
|
||||||
|
- [Import Changes](#import-changes)
|
||||||
|
- [API Changes](#api-changes)
|
||||||
|
- [Mocking](#mocking)
|
||||||
|
- [Common Patterns](#common-patterns)
|
||||||
|
- [Examples](#examples)
|
||||||
|
|
||||||
|
## Basic Setup
|
||||||
|
|
||||||
|
1. Remove Jest-related dependencies from `package.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"@jest/globals": "...",
|
||||||
|
"jest": "...",
|
||||||
|
"ts-jest": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Remove Jest configuration files:
|
||||||
|
- `jest.config.js`
|
||||||
|
- `jest.setup.js`
|
||||||
|
|
||||||
|
3. Update test scripts in `package.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"test": "bun test",
|
||||||
|
"test:watch": "bun test --watch",
|
||||||
|
"test:coverage": "bun test --coverage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Import Changes
|
||||||
|
|
||||||
|
### Before (Jest):
|
||||||
|
```typescript
|
||||||
|
import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
||||||
|
```
|
||||||
|
|
||||||
|
### After (Bun):
|
||||||
|
```typescript
|
||||||
|
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
|
||||||
|
import type { Mock } from "bun:test";
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `it` is replaced with `test` in Bun.
|
||||||
|
|
||||||
|
## API Changes
|
||||||
|
|
||||||
|
### Test Structure
|
||||||
|
```typescript
|
||||||
|
// Jest
|
||||||
|
describe('Suite', () => {
|
||||||
|
it('should do something', () => {
|
||||||
|
// test
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bun
|
||||||
|
describe('Suite', () => {
|
||||||
|
test('should do something', () => {
|
||||||
|
// test
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Assertions
|
||||||
|
Most Jest assertions work the same in Bun:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// These work the same in both:
|
||||||
|
expect(value).toBe(expected);
|
||||||
|
expect(value).toEqual(expected);
|
||||||
|
expect(value).toBeDefined();
|
||||||
|
expect(value).toBeUndefined();
|
||||||
|
expect(value).toBeTruthy();
|
||||||
|
expect(value).toBeFalsy();
|
||||||
|
expect(array).toContain(item);
|
||||||
|
expect(value).toBeInstanceOf(Class);
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
expect(spy).toHaveBeenCalledWith(...args);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mocking
|
||||||
|
|
||||||
|
### Function Mocking
|
||||||
|
|
||||||
|
#### Before (Jest):
|
||||||
|
```typescript
|
||||||
|
const mockFn = jest.fn();
|
||||||
|
mockFn.mockImplementation(() => 'result');
|
||||||
|
mockFn.mockResolvedValue('result');
|
||||||
|
mockFn.mockRejectedValue(new Error());
|
||||||
|
```
|
||||||
|
|
||||||
|
#### After (Bun):
|
||||||
|
```typescript
|
||||||
|
const mockFn = mock(() => 'result');
|
||||||
|
const mockAsyncFn = mock(() => Promise.resolve('result'));
|
||||||
|
const mockErrorFn = mock(() => Promise.reject(new Error()));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Module Mocking
|
||||||
|
|
||||||
|
#### Before (Jest):
|
||||||
|
```typescript
|
||||||
|
jest.mock('module-name', () => ({
|
||||||
|
default: jest.fn(),
|
||||||
|
namedExport: jest.fn()
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
#### After (Bun):
|
||||||
|
```typescript
|
||||||
|
// Option 1: Using vi.mock (if available)
|
||||||
|
vi.mock('module-name', () => ({
|
||||||
|
default: mock(() => {}),
|
||||||
|
namedExport: mock(() => {})
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Option 2: Using dynamic imports
|
||||||
|
const mockModule = {
|
||||||
|
default: mock(() => {}),
|
||||||
|
namedExport: mock(() => {})
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mock Reset/Clear
|
||||||
|
|
||||||
|
#### Before (Jest):
|
||||||
|
```typescript
|
||||||
|
jest.clearAllMocks();
|
||||||
|
mockFn.mockClear();
|
||||||
|
jest.resetModules();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### After (Bun):
|
||||||
|
```typescript
|
||||||
|
mockFn.mockReset();
|
||||||
|
// or for specific calls
|
||||||
|
mockFn.mock.calls = [];
|
||||||
|
```
|
||||||
|
|
||||||
|
### Spy on Methods
|
||||||
|
|
||||||
|
#### Before (Jest):
|
||||||
|
```typescript
|
||||||
|
jest.spyOn(object, 'method');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### After (Bun):
|
||||||
|
```typescript
|
||||||
|
const spy = mock(((...args) => object.method(...args)));
|
||||||
|
object.method = spy;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Async Tests
|
||||||
|
```typescript
|
||||||
|
// Works the same in both Jest and Bun:
|
||||||
|
test('async test', async () => {
|
||||||
|
const result = await someAsyncFunction();
|
||||||
|
expect(result).toBe(expected);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setup and Teardown
|
||||||
|
```typescript
|
||||||
|
describe('Suite', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// setup
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// cleanup
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test', () => {
|
||||||
|
// test
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mocking Fetch
|
||||||
|
```typescript
|
||||||
|
// Before (Jest)
|
||||||
|
global.fetch = jest.fn(() => Promise.resolve(new Response()));
|
||||||
|
|
||||||
|
// After (Bun)
|
||||||
|
const mockFetch = mock(() => Promise.resolve(new Response()));
|
||||||
|
global.fetch = mockFetch as unknown as typeof fetch;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mocking WebSocket
|
||||||
|
```typescript
|
||||||
|
// Create a MockWebSocket class implementing WebSocket interface
|
||||||
|
class MockWebSocket implements WebSocket {
|
||||||
|
public static readonly CONNECTING = 0;
|
||||||
|
public static readonly OPEN = 1;
|
||||||
|
public static readonly CLOSING = 2;
|
||||||
|
public static readonly CLOSED = 3;
|
||||||
|
|
||||||
|
public readyState: 0 | 1 | 2 | 3 = MockWebSocket.OPEN;
|
||||||
|
public addEventListener = mock(() => undefined);
|
||||||
|
public removeEventListener = mock(() => undefined);
|
||||||
|
public send = mock(() => undefined);
|
||||||
|
public close = mock(() => undefined);
|
||||||
|
// ... implement other required methods
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use it in tests
|
||||||
|
global.WebSocket = MockWebSocket as unknown as typeof WebSocket;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Basic Test
|
||||||
|
```typescript
|
||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
|
describe('formatToolCall', () => {
|
||||||
|
test('should format an object into the correct structure', () => {
|
||||||
|
const testObj = { name: 'test', value: 123 };
|
||||||
|
const result = formatToolCall(testObj);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
content: [{
|
||||||
|
type: 'text',
|
||||||
|
text: JSON.stringify(testObj, null, 2),
|
||||||
|
isError: false
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Async Test with Mocking
|
||||||
|
```typescript
|
||||||
|
import { describe, expect, test, mock } from "bun:test";
|
||||||
|
|
||||||
|
describe('API Client', () => {
|
||||||
|
test('should fetch data', async () => {
|
||||||
|
const mockResponse = { data: 'test' };
|
||||||
|
const mockFetch = mock(() => Promise.resolve(new Response(
|
||||||
|
JSON.stringify(mockResponse),
|
||||||
|
{ status: 200, headers: new Headers() }
|
||||||
|
)));
|
||||||
|
global.fetch = mockFetch as unknown as typeof fetch;
|
||||||
|
|
||||||
|
const result = await apiClient.getData();
|
||||||
|
expect(result).toEqual(mockResponse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complex Mocking Example
|
||||||
|
```typescript
|
||||||
|
import { describe, expect, test, mock } from "bun:test";
|
||||||
|
import type { Mock } from "bun:test";
|
||||||
|
|
||||||
|
interface MockServices {
|
||||||
|
light: {
|
||||||
|
turn_on: Mock<() => Promise<{ success: boolean }>>;
|
||||||
|
turn_off: Mock<() => Promise<{ success: boolean }>>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockServices: MockServices = {
|
||||||
|
light: {
|
||||||
|
turn_on: mock(() => Promise.resolve({ success: true })),
|
||||||
|
turn_off: mock(() => Promise.resolve({ success: true }))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Home Assistant Service', () => {
|
||||||
|
test('should control lights', async () => {
|
||||||
|
const result = await mockServices.light.turn_on();
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. Use TypeScript for better type safety in mocks
|
||||||
|
2. Keep mocks as simple as possible
|
||||||
|
3. Prefer interface-based mocks over concrete implementations
|
||||||
|
4. Use proper type assertions when necessary
|
||||||
|
5. Clean up mocks in `afterEach` blocks
|
||||||
|
6. Use descriptive test names
|
||||||
|
7. Group related tests using `describe` blocks
|
||||||
|
|
||||||
|
## Common Issues and Solutions
|
||||||
|
|
||||||
|
### Issue: Type Errors with Mocks
|
||||||
|
```typescript
|
||||||
|
// Solution: Use proper typing with Mock type
|
||||||
|
import type { Mock } from "bun:test";
|
||||||
|
const mockFn: Mock<() => string> = mock(() => "result");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: Global Object Mocking
|
||||||
|
```typescript
|
||||||
|
// Solution: Use type assertions carefully
|
||||||
|
global.someGlobal = mockImplementation as unknown as typeof someGlobal;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: Module Mocking
|
||||||
|
```typescript
|
||||||
|
// Solution: Use dynamic imports or vi.mock if available
|
||||||
|
const mockModule = {
|
||||||
|
default: mock(() => mockImplementation)
|
||||||
|
};
|
||||||
|
```
|
||||||
226
docs/development/tools.md
Normal file
226
docs/development/tools.md
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
# Tool Development Guide
|
||||||
|
|
||||||
|
This guide explains how to create new tools for the Home Assistant MCP.
|
||||||
|
|
||||||
|
## Tool Structure
|
||||||
|
|
||||||
|
Each tool should follow this basic structure:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Tool {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
version: string;
|
||||||
|
category: ToolCategory;
|
||||||
|
execute(params: any): Promise<ToolResult>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating a New Tool
|
||||||
|
|
||||||
|
1. Create a new file in the appropriate category directory
|
||||||
|
2. Implement the Tool interface
|
||||||
|
3. Add API endpoints
|
||||||
|
4. Add WebSocket handlers
|
||||||
|
5. Add documentation
|
||||||
|
6. Add tests
|
||||||
|
|
||||||
|
### Example Tool Implementation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Tool, ToolCategory, ToolResult } from '../interfaces';
|
||||||
|
|
||||||
|
export class MyCustomTool implements Tool {
|
||||||
|
id = 'my_custom_tool';
|
||||||
|
name = 'My Custom Tool';
|
||||||
|
description = 'Description of what the tool does';
|
||||||
|
version = '1.0.0';
|
||||||
|
category = ToolCategory.Utility;
|
||||||
|
|
||||||
|
async execute(params: any): Promise<ToolResult> {
|
||||||
|
// Tool implementation
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
// Tool-specific response data
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tool Categories
|
||||||
|
|
||||||
|
- Device Management
|
||||||
|
- History & State
|
||||||
|
- Automation
|
||||||
|
- Add-ons & Packages
|
||||||
|
- Notifications
|
||||||
|
- Events
|
||||||
|
- Utility
|
||||||
|
|
||||||
|
## API Integration
|
||||||
|
|
||||||
|
### REST Endpoint
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Router } from 'express';
|
||||||
|
import { MyCustomTool } from './my-custom-tool';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
const tool = new MyCustomTool();
|
||||||
|
|
||||||
|
router.post('/api/tools/custom', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await tool.execute(req.body);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket Handler
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { WebSocketServer } from 'ws';
|
||||||
|
import { MyCustomTool } from './my-custom-tool';
|
||||||
|
|
||||||
|
const tool = new MyCustomTool();
|
||||||
|
|
||||||
|
wss.on('connection', (ws) => {
|
||||||
|
ws.on('message', async (message) => {
|
||||||
|
const { type, params } = JSON.parse(message);
|
||||||
|
if (type === 'my_custom_tool') {
|
||||||
|
const result = await tool.execute(params);
|
||||||
|
ws.send(JSON.stringify(result));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class ToolError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public code: string,
|
||||||
|
public status: number = 500
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ToolError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage in tool
|
||||||
|
async execute(params: any): Promise<ToolResult> {
|
||||||
|
try {
|
||||||
|
// Tool implementation
|
||||||
|
} catch (error) {
|
||||||
|
throw new ToolError(
|
||||||
|
'Operation failed',
|
||||||
|
'TOOL_ERROR',
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { MyCustomTool } from './my-custom-tool';
|
||||||
|
|
||||||
|
describe('MyCustomTool', () => {
|
||||||
|
let tool: MyCustomTool;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tool = new MyCustomTool();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should execute successfully', async () => {
|
||||||
|
const result = await tool.execute({
|
||||||
|
// Test parameters
|
||||||
|
});
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors', async () => {
|
||||||
|
// Error test cases
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
1. Create tool documentation in `docs/tools/category/tool-name.md`
|
||||||
|
2. Update `tools/tools.md` with tool reference
|
||||||
|
3. Add tool to navigation in `mkdocs.yml`
|
||||||
|
|
||||||
|
### Documentation Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Tool Name
|
||||||
|
|
||||||
|
Description of the tool.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Feature 1
|
||||||
|
- Feature 2
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// API endpoints
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// WebSocket usage
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Example 1
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Usage example
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
// Response data structure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. Follow consistent naming conventions
|
||||||
|
2. Implement proper error handling
|
||||||
|
3. Add comprehensive documentation
|
||||||
|
4. Write thorough tests
|
||||||
|
5. Use TypeScript for type safety
|
||||||
|
6. Follow SOLID principles
|
||||||
|
7. Implement rate limiting
|
||||||
|
8. Add proper logging
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Interface Documentation](interfaces.md)
|
||||||
|
- [Best Practices](best-practices.md)
|
||||||
|
- [Testing Guide](../testing.md)
|
||||||
22
docs/examples/index.md
Normal file
22
docs/examples/index.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
layout: default
|
||||||
|
title: Examples
|
||||||
|
nav_order: 7
|
||||||
|
has_children: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Example Projects 📚
|
||||||
|
|
||||||
|
This section contains examples and tutorials for common MCP Server integrations.
|
||||||
|
|
||||||
|
## Speech-to-Text Integration
|
||||||
|
|
||||||
|
Example of integrating speech recognition with MCP Server:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// From examples/speech-to-text-example.ts
|
||||||
|
// Add example code and explanation
|
||||||
|
```
|
||||||
|
|
||||||
|
## More Examples Coming Soon
|
||||||
|
...
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
Begin your journey with the Home Assistant MCP Server by following these steps:
|
Begin your journey with the Home Assistant MCP Server by following these steps:
|
||||||
|
|
||||||
- **API Documentation:** Read the [API Documentation](api.md) for available endpoints.
|
- **API Documentation:** Read the [API Documentation](api.md) for available endpoints.
|
||||||
- **Real-Time Updates:** Learn about [Server-Sent Events](sse-api.md) for live communication.
|
- **Real-Time Updates:** Learn about [Server-Sent Events](api/sse.md) for live communication.
|
||||||
- **Tools:** Explore available [Tools](tools/tools.md) for device control and automation.
|
- **Tools:** Explore available [Tools](tools/tools.md) for device control and automation.
|
||||||
- **Configuration:** Refer to the [Configuration Guide](configuration.md) for setup and advanced settings.
|
- **Configuration:** Refer to the [Configuration Guide](getting-started/configuration.md) for setup and advanced settings.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
10
docs/getting-started/docker.md
Normal file
10
docs/getting-started/docker.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
layout: default
|
||||||
|
title: Docker Deployment
|
||||||
|
parent: Getting Started
|
||||||
|
nav_order: 3
|
||||||
|
---
|
||||||
|
|
||||||
|
# Docker Deployment Guide 🐳
|
||||||
|
|
||||||
|
Detailed guide for deploying MCP Server with Docker...
|
||||||
@@ -23,6 +23,16 @@ Before installing MCP Server, ensure you have:
|
|||||||
|
|
||||||
The easiest way to install MCP Server is through Smithery:
|
The easiest way to install MCP Server is through Smithery:
|
||||||
|
|
||||||
|
#### Smithery Configuration
|
||||||
|
|
||||||
|
The project includes a `smithery.yaml` configuration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Add smithery.yaml contents and explanation
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Installation Steps
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx -y @smithery/cli install @jango-blockchained/advanced-homeassistant-mcp --client claude
|
npx -y @smithery/cli install @jango-blockchained/advanced-homeassistant-mcp --client claude
|
||||||
```
|
```
|
||||||
|
|||||||
364
docs/sse-api.md
364
docs/sse-api.md
@@ -1,364 +0,0 @@
|
|||||||
# Home Assistant MCP Server-Sent Events (SSE) API Documentation
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The SSE API provides real-time updates from Home Assistant through a persistent connection. This allows clients to receive instant notifications about state changes, events, and other activities without polling.
|
|
||||||
|
|
||||||
## Quick Reference
|
|
||||||
|
|
||||||
### Available Endpoints
|
|
||||||
|
|
||||||
| Endpoint | Method | Description | Authentication |
|
|
||||||
|----------|---------|-------------|----------------|
|
|
||||||
| `/subscribe_events` | POST | Subscribe to real-time events and state changes | Required |
|
|
||||||
| `/get_sse_stats` | POST | Get statistics about current SSE connections | Required |
|
|
||||||
|
|
||||||
### Event Types Available
|
|
||||||
|
|
||||||
| Event Type | Description | Example Subscription |
|
|
||||||
|------------|-------------|---------------------|
|
|
||||||
| `state_changed` | Entity state changes | `events=state_changed` |
|
|
||||||
| `service_called` | Service call events | `events=service_called` |
|
|
||||||
| `automation_triggered` | Automation trigger events | `events=automation_triggered` |
|
|
||||||
| `script_executed` | Script execution events | `events=script_executed` |
|
|
||||||
| `ping` | Connection keepalive (system) | Automatic |
|
|
||||||
| `error` | Error notifications (system) | Automatic |
|
|
||||||
|
|
||||||
### Subscription Options
|
|
||||||
|
|
||||||
| Option | Description | Example |
|
|
||||||
|--------|-------------|---------|
|
|
||||||
| `entity_id` | Subscribe to specific entity | `entity_id=light.living_room` |
|
|
||||||
| `domain` | Subscribe to entire domain | `domain=light` |
|
|
||||||
| `events` | Subscribe to event types | `events=state_changed,automation_triggered` |
|
|
||||||
|
|
||||||
## Authentication
|
|
||||||
|
|
||||||
All SSE connections require authentication using your Home Assistant token.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const token = 'YOUR_HASS_TOKEN';
|
|
||||||
```
|
|
||||||
|
|
||||||
## Endpoints
|
|
||||||
|
|
||||||
### Subscribe to Events
|
|
||||||
|
|
||||||
`POST /subscribe_events`
|
|
||||||
|
|
||||||
Subscribe to Home Assistant events and state changes.
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
|------------|----------|----------|-------------|
|
|
||||||
| token | string | Yes | Your Home Assistant authentication token |
|
|
||||||
| events | string[] | No | Array of event types to subscribe to |
|
|
||||||
| entity_id | string | No | Specific entity ID to monitor |
|
|
||||||
| domain | string | No | Domain to monitor (e.g., "light", "switch") |
|
|
||||||
|
|
||||||
#### Example Request
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const eventSource = new EventSource(`http://localhost:3000/subscribe_events?token=${token}&entity_id=light.living_room&domain=switch&events=state_changed,automation_triggered`);
|
|
||||||
|
|
||||||
eventSource.onmessage = (event) => {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
console.log('Received:', data);
|
|
||||||
};
|
|
||||||
|
|
||||||
eventSource.onerror = (error) => {
|
|
||||||
console.error('SSE Error:', error);
|
|
||||||
eventSource.close();
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Get SSE Statistics
|
|
||||||
|
|
||||||
`POST /get_sse_stats`
|
|
||||||
|
|
||||||
Get current statistics about SSE connections and subscriptions.
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
|-----------|--------|----------|-------------|
|
|
||||||
| token | string | Yes | Your Home Assistant authentication token |
|
|
||||||
|
|
||||||
#### Example Request
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:3000/get_sse_stats \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"token": "YOUR_HASS_TOKEN"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Event Types
|
|
||||||
|
|
||||||
### Standard Events
|
|
||||||
|
|
||||||
1. **connection**
|
|
||||||
- Sent when a client connects successfully
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "connection",
|
|
||||||
"status": "connected",
|
|
||||||
"id": "client_uuid",
|
|
||||||
"authenticated": true,
|
|
||||||
"timestamp": "2024-02-10T12:00:00.000Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **state_changed**
|
|
||||||
- Sent when an entity's state changes
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "state_changed",
|
|
||||||
"data": {
|
|
||||||
"entity_id": "light.living_room",
|
|
||||||
"state": "on",
|
|
||||||
"attributes": {
|
|
||||||
"brightness": 255,
|
|
||||||
"color_temp": 370
|
|
||||||
},
|
|
||||||
"last_changed": "2024-02-10T12:00:00.000Z",
|
|
||||||
"last_updated": "2024-02-10T12:00:00.000Z"
|
|
||||||
},
|
|
||||||
"timestamp": "2024-02-10T12:00:00.000Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **service_called**
|
|
||||||
- Sent when a Home Assistant service is called
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "service_called",
|
|
||||||
"data": {
|
|
||||||
"domain": "light",
|
|
||||||
"service": "turn_on",
|
|
||||||
"service_data": {
|
|
||||||
"entity_id": "light.living_room",
|
|
||||||
"brightness": 255
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"timestamp": "2024-02-10T12:00:00.000Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **automation_triggered**
|
|
||||||
- Sent when an automation is triggered
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "automation_triggered",
|
|
||||||
"data": {
|
|
||||||
"automation_id": "automation.morning_routine",
|
|
||||||
"trigger": {
|
|
||||||
"platform": "time",
|
|
||||||
"at": "07:00:00"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"timestamp": "2024-02-10T12:00:00.000Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
5. **script_executed**
|
|
||||||
- Sent when a script is executed
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "script_executed",
|
|
||||||
"data": {
|
|
||||||
"script_id": "script.welcome_home",
|
|
||||||
"execution_data": {
|
|
||||||
"status": "completed"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"timestamp": "2024-02-10T12:00:00.000Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### System Events
|
|
||||||
|
|
||||||
1. **ping**
|
|
||||||
- Sent every 30 seconds to keep the connection alive
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "ping",
|
|
||||||
"timestamp": "2024-02-10T12:00:00.000Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **error**
|
|
||||||
- Sent when an error occurs
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "error",
|
|
||||||
"error": "rate_limit_exceeded",
|
|
||||||
"message": "Too many requests, please try again later",
|
|
||||||
"timestamp": "2024-02-10T12:00:00.000Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Rate Limiting
|
|
||||||
|
|
||||||
- Maximum 1000 requests per minute per client
|
|
||||||
- Rate limits are reset every minute
|
|
||||||
- Exceeding the rate limit will result in an error event
|
|
||||||
|
|
||||||
## Connection Management
|
|
||||||
|
|
||||||
- Maximum 100 concurrent clients
|
|
||||||
- Connections timeout after 5 minutes of inactivity
|
|
||||||
- Ping messages are sent every 30 seconds
|
|
||||||
- Clients should handle reconnection on connection loss
|
|
||||||
|
|
||||||
## Example Implementation
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
class HomeAssistantSSE {
|
|
||||||
constructor(baseUrl, token) {
|
|
||||||
this.baseUrl = baseUrl;
|
|
||||||
this.token = token;
|
|
||||||
this.eventSource = null;
|
|
||||||
this.reconnectAttempts = 0;
|
|
||||||
this.maxReconnectAttempts = 5;
|
|
||||||
this.reconnectDelay = 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
connect(options = {}) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
token: this.token,
|
|
||||||
...(options.events && { events: options.events.join(',') }),
|
|
||||||
...(options.entity_id && { entity_id: options.entity_id }),
|
|
||||||
...(options.domain && { domain: options.domain })
|
|
||||||
});
|
|
||||||
|
|
||||||
this.eventSource = new EventSource(`${this.baseUrl}/subscribe_events?${params}`);
|
|
||||||
|
|
||||||
this.eventSource.onmessage = (event) => {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
this.handleEvent(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.eventSource.onerror = (error) => {
|
|
||||||
console.error('SSE Error:', error);
|
|
||||||
this.handleError(error);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEvent(data) {
|
|
||||||
switch (data.type) {
|
|
||||||
case 'connection':
|
|
||||||
this.reconnectAttempts = 0;
|
|
||||||
console.log('Connected:', data);
|
|
||||||
break;
|
|
||||||
case 'ping':
|
|
||||||
// Connection is alive
|
|
||||||
break;
|
|
||||||
case 'error':
|
|
||||||
console.error('Server Error:', data);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Handle other event types
|
|
||||||
console.log('Event:', data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleError(error) {
|
|
||||||
this.eventSource?.close();
|
|
||||||
|
|
||||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
||||||
this.reconnectAttempts++;
|
|
||||||
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
||||||
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
||||||
setTimeout(() => this.connect(), delay);
|
|
||||||
} else {
|
|
||||||
console.error('Max reconnection attempts reached');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnect() {
|
|
||||||
this.eventSource?.close();
|
|
||||||
this.eventSource = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage example
|
|
||||||
const client = new HomeAssistantSSE('http://localhost:3000', 'YOUR_HASS_TOKEN');
|
|
||||||
client.connect({
|
|
||||||
events: ['state_changed', 'automation_triggered'],
|
|
||||||
domain: 'light'
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Error Handling**
|
|
||||||
- Implement exponential backoff for reconnection attempts
|
|
||||||
- Handle connection timeouts gracefully
|
|
||||||
- Monitor for rate limit errors
|
|
||||||
|
|
||||||
2. **Resource Management**
|
|
||||||
- Close EventSource when no longer needed
|
|
||||||
- Limit subscriptions to necessary events/entities
|
|
||||||
- Handle cleanup on page unload
|
|
||||||
|
|
||||||
3. **Security**
|
|
||||||
- Never expose the authentication token in client-side code
|
|
||||||
- Use HTTPS in production
|
|
||||||
- Validate all incoming data
|
|
||||||
|
|
||||||
4. **Performance**
|
|
||||||
- Subscribe only to needed events
|
|
||||||
- Implement client-side event filtering
|
|
||||||
- Monitor memory usage for long-running connections
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
1. **Connection Failures**
|
|
||||||
- Verify your authentication token is valid
|
|
||||||
- Check server URL is accessible
|
|
||||||
- Ensure proper network connectivity
|
|
||||||
- Verify SSL/TLS configuration if using HTTPS
|
|
||||||
|
|
||||||
2. **Missing Events**
|
|
||||||
- Confirm subscription parameters are correct
|
|
||||||
- Check rate limiting status
|
|
||||||
- Verify entity/domain exists
|
|
||||||
- Monitor client-side event handlers
|
|
||||||
|
|
||||||
3. **Performance Issues**
|
|
||||||
- Reduce number of subscriptions
|
|
||||||
- Implement client-side filtering
|
|
||||||
- Monitor memory usage
|
|
||||||
- Check network latency
|
|
||||||
|
|
||||||
### Debugging Tips
|
|
||||||
|
|
||||||
1. Enable console logging:
|
|
||||||
```javascript
|
|
||||||
const client = new HomeAssistantSSE('http://localhost:3000', 'YOUR_HASS_TOKEN');
|
|
||||||
client.debug = true; // Enables detailed logging
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Monitor network traffic:
|
|
||||||
```javascript
|
|
||||||
// Add event listeners for connection states
|
|
||||||
eventSource.addEventListener('open', () => {
|
|
||||||
console.log('Connection opened');
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.addEventListener('error', (e) => {
|
|
||||||
console.log('Connection error:', e);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Track subscription status:
|
|
||||||
```javascript
|
|
||||||
// Get current subscriptions
|
|
||||||
const stats = await fetch('/get_sse_stats', {
|
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
|
||||||
}).then(r => r.json());
|
|
||||||
|
|
||||||
console.log('Current subscriptions:', stats);
|
|
||||||
```
|
|
||||||
240
docs/tools/addons-packages/addon.md
Normal file
240
docs/tools/addons-packages/addon.md
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
# Add-on Management Tool
|
||||||
|
|
||||||
|
The Add-on Management tool provides functionality to manage Home Assistant add-ons through the MCP interface.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- List available add-ons
|
||||||
|
- Install/uninstall add-ons
|
||||||
|
- Start/stop/restart add-ons
|
||||||
|
- Get add-on information
|
||||||
|
- Update add-ons
|
||||||
|
- Configure add-ons
|
||||||
|
- View add-on logs
|
||||||
|
- Monitor add-on status
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
GET /api/addons
|
||||||
|
GET /api/addons/{addon_slug}
|
||||||
|
POST /api/addons/{addon_slug}/install
|
||||||
|
POST /api/addons/{addon_slug}/uninstall
|
||||||
|
POST /api/addons/{addon_slug}/start
|
||||||
|
POST /api/addons/{addon_slug}/stop
|
||||||
|
POST /api/addons/{addon_slug}/restart
|
||||||
|
GET /api/addons/{addon_slug}/logs
|
||||||
|
PUT /api/addons/{addon_slug}/config
|
||||||
|
GET /api/addons/{addon_slug}/stats
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// List add-ons
|
||||||
|
{
|
||||||
|
"type": "get_addons"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get add-on info
|
||||||
|
{
|
||||||
|
"type": "get_addon_info",
|
||||||
|
"addon_slug": "required_addon_slug"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install add-on
|
||||||
|
{
|
||||||
|
"type": "install_addon",
|
||||||
|
"addon_slug": "required_addon_slug",
|
||||||
|
"version": "optional_version"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control add-on
|
||||||
|
{
|
||||||
|
"type": "control_addon",
|
||||||
|
"addon_slug": "required_addon_slug",
|
||||||
|
"action": "start|stop|restart"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### List All Add-ons
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/addons', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const addons = await response.json();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install Add-on
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/addons/mosquitto/install', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"version": "latest"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configure Add-on
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/addons/mosquitto/config', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"logins": [
|
||||||
|
{
|
||||||
|
"username": "mqtt_user",
|
||||||
|
"password": "mqtt_password"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"customize": {
|
||||||
|
"active": true,
|
||||||
|
"folder": "mosquitto"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
### Add-on List Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"addons": [
|
||||||
|
{
|
||||||
|
"slug": "addon_slug",
|
||||||
|
"name": "Add-on Name",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"state": "started",
|
||||||
|
"repository": "core",
|
||||||
|
"installed": true,
|
||||||
|
"update_available": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add-on Info Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"addon": {
|
||||||
|
"slug": "addon_slug",
|
||||||
|
"name": "Add-on Name",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Add-on description",
|
||||||
|
"long_description": "Detailed description",
|
||||||
|
"repository": "core",
|
||||||
|
"installed": true,
|
||||||
|
"state": "started",
|
||||||
|
"webui": "http://[HOST]:[PORT:80]",
|
||||||
|
"boot": "auto",
|
||||||
|
"options": {
|
||||||
|
// Add-on specific options
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
// Add-on options schema
|
||||||
|
},
|
||||||
|
"ports": {
|
||||||
|
"80/tcp": 8080
|
||||||
|
},
|
||||||
|
"ingress": true,
|
||||||
|
"ingress_port": 8099
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add-on Stats Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"stats": {
|
||||||
|
"cpu_percent": 2.5,
|
||||||
|
"memory_usage": 128974848,
|
||||||
|
"memory_limit": 536870912,
|
||||||
|
"network_rx": 1234,
|
||||||
|
"network_tx": 5678,
|
||||||
|
"blk_read": 12345,
|
||||||
|
"blk_write": 67890
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Error Codes
|
||||||
|
|
||||||
|
- `404`: Add-on not found
|
||||||
|
- `401`: Unauthorized
|
||||||
|
- `400`: Invalid request
|
||||||
|
- `409`: Add-on operation failed
|
||||||
|
- `422`: Invalid configuration
|
||||||
|
|
||||||
|
### Error Response Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Error description",
|
||||||
|
"error_code": "ERROR_CODE"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
- Default limit: 50 requests per 15 minutes
|
||||||
|
- Configurable through environment variables:
|
||||||
|
- `ADDON_RATE_LIMIT`
|
||||||
|
- `ADDON_RATE_WINDOW`
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. Always check add-on compatibility
|
||||||
|
2. Back up configurations before updates
|
||||||
|
3. Monitor resource usage
|
||||||
|
4. Use appropriate update strategies
|
||||||
|
5. Implement proper error handling
|
||||||
|
6. Test configurations in safe environment
|
||||||
|
7. Handle rate limiting gracefully
|
||||||
|
8. Keep add-ons updated
|
||||||
|
|
||||||
|
## Add-on Security
|
||||||
|
|
||||||
|
- Use secure passwords
|
||||||
|
- Regularly update add-ons
|
||||||
|
- Monitor add-on logs
|
||||||
|
- Restrict network access
|
||||||
|
- Use SSL/TLS when available
|
||||||
|
- Follow principle of least privilege
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Package Management](package.md)
|
||||||
|
- [Device Control](../device-management/control.md)
|
||||||
|
- [Event Subscription](../events/subscribe-events.md)
|
||||||
236
docs/tools/addons-packages/package.md
Normal file
236
docs/tools/addons-packages/package.md
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# Package Management Tool
|
||||||
|
|
||||||
|
The Package Management tool provides functionality to manage Home Assistant Community Store (HACS) packages through the MCP interface.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- List available packages
|
||||||
|
- Install/update/remove packages
|
||||||
|
- Search packages
|
||||||
|
- Get package information
|
||||||
|
- Manage package repositories
|
||||||
|
- Track package updates
|
||||||
|
- View package documentation
|
||||||
|
- Monitor package status
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
GET /api/packages
|
||||||
|
GET /api/packages/{package_id}
|
||||||
|
POST /api/packages/{package_id}/install
|
||||||
|
POST /api/packages/{package_id}/uninstall
|
||||||
|
POST /api/packages/{package_id}/update
|
||||||
|
GET /api/packages/search
|
||||||
|
GET /api/packages/categories
|
||||||
|
GET /api/packages/repositories
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// List packages
|
||||||
|
{
|
||||||
|
"type": "get_packages",
|
||||||
|
"category": "optional_category"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search packages
|
||||||
|
{
|
||||||
|
"type": "search_packages",
|
||||||
|
"query": "search_query",
|
||||||
|
"category": "optional_category"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install package
|
||||||
|
{
|
||||||
|
"type": "install_package",
|
||||||
|
"package_id": "required_package_id",
|
||||||
|
"version": "optional_version"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Package Categories
|
||||||
|
|
||||||
|
- Integrations
|
||||||
|
- Frontend
|
||||||
|
- Themes
|
||||||
|
- AppDaemon Apps
|
||||||
|
- NetDaemon Apps
|
||||||
|
- Python Scripts
|
||||||
|
- Plugins
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### List All Packages
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/packages', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const packages = await response.json();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search Packages
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/packages/search?q=weather&category=integrations', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const searchResults = await response.json();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install Package
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/packages/custom-weather-card/install', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"version": "latest"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
### Package List Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"id": "package_id",
|
||||||
|
"name": "Package Name",
|
||||||
|
"category": "integrations",
|
||||||
|
"description": "Package description",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"installed": true,
|
||||||
|
"update_available": false,
|
||||||
|
"stars": 150,
|
||||||
|
"downloads": 10000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Package Info Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"package": {
|
||||||
|
"id": "package_id",
|
||||||
|
"name": "Package Name",
|
||||||
|
"category": "integrations",
|
||||||
|
"description": "Package description",
|
||||||
|
"long_description": "Detailed description",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"installed_version": "0.9.0",
|
||||||
|
"available_version": "1.0.0",
|
||||||
|
"installed": true,
|
||||||
|
"update_available": true,
|
||||||
|
"stars": 150,
|
||||||
|
"downloads": 10000,
|
||||||
|
"repository": "https://github.com/author/repo",
|
||||||
|
"author": {
|
||||||
|
"name": "Author Name",
|
||||||
|
"url": "https://github.com/author"
|
||||||
|
},
|
||||||
|
"documentation": "https://github.com/author/repo/wiki",
|
||||||
|
"dependencies": [
|
||||||
|
"dependency1",
|
||||||
|
"dependency2"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": "package_id",
|
||||||
|
"name": "Package Name",
|
||||||
|
"category": "integrations",
|
||||||
|
"description": "Package description",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"score": 0.95
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 42
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Error Codes
|
||||||
|
|
||||||
|
- `404`: Package not found
|
||||||
|
- `401`: Unauthorized
|
||||||
|
- `400`: Invalid request
|
||||||
|
- `409`: Package operation failed
|
||||||
|
- `422`: Invalid configuration
|
||||||
|
- `424`: Dependency error
|
||||||
|
|
||||||
|
### Error Response Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Error description",
|
||||||
|
"error_code": "ERROR_CODE"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
- Default limit: 50 requests per 15 minutes
|
||||||
|
- Configurable through environment variables:
|
||||||
|
- `PACKAGE_RATE_LIMIT`
|
||||||
|
- `PACKAGE_RATE_WINDOW`
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. Check package compatibility
|
||||||
|
2. Review package documentation
|
||||||
|
3. Verify package dependencies
|
||||||
|
4. Back up before updates
|
||||||
|
5. Test in safe environment
|
||||||
|
6. Monitor resource usage
|
||||||
|
7. Keep packages updated
|
||||||
|
8. Handle rate limiting gracefully
|
||||||
|
|
||||||
|
## Package Security
|
||||||
|
|
||||||
|
- Verify package sources
|
||||||
|
- Review package permissions
|
||||||
|
- Check package reputation
|
||||||
|
- Monitor package activity
|
||||||
|
- Keep dependencies updated
|
||||||
|
- Follow security advisories
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Add-on Management](addon.md)
|
||||||
|
- [Device Control](../device-management/control.md)
|
||||||
|
- [Event Subscription](../events/subscribe-events.md)
|
||||||
321
docs/tools/automation/automation-config.md
Normal file
321
docs/tools/automation/automation-config.md
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
# Automation Configuration Tool
|
||||||
|
|
||||||
|
The Automation Configuration tool provides functionality to create, update, and manage Home Assistant automation configurations.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Create new automations
|
||||||
|
- Update existing automations
|
||||||
|
- Delete automations
|
||||||
|
- Duplicate automations
|
||||||
|
- Import/Export automation configurations
|
||||||
|
- Validate automation configurations
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
POST /api/automations
|
||||||
|
PUT /api/automations/{automation_id}
|
||||||
|
DELETE /api/automations/{automation_id}
|
||||||
|
POST /api/automations/{automation_id}/duplicate
|
||||||
|
POST /api/automations/validate
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create automation
|
||||||
|
{
|
||||||
|
"type": "create_automation",
|
||||||
|
"automation": {
|
||||||
|
// Automation configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update automation
|
||||||
|
{
|
||||||
|
"type": "update_automation",
|
||||||
|
"automation_id": "required_automation_id",
|
||||||
|
"automation": {
|
||||||
|
// Updated configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete automation
|
||||||
|
{
|
||||||
|
"type": "delete_automation",
|
||||||
|
"automation_id": "required_automation_id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Automation Configuration
|
||||||
|
|
||||||
|
### Basic Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "morning_routine",
|
||||||
|
"alias": "Morning Routine",
|
||||||
|
"description": "Turn on lights and adjust temperature in the morning",
|
||||||
|
"trigger": [
|
||||||
|
{
|
||||||
|
"platform": "time",
|
||||||
|
"at": "07:00:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"condition": [
|
||||||
|
{
|
||||||
|
"condition": "time",
|
||||||
|
"weekday": ["mon", "tue", "wed", "thu", "fri"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"action": [
|
||||||
|
{
|
||||||
|
"service": "light.turn_on",
|
||||||
|
"target": {
|
||||||
|
"entity_id": "light.bedroom"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"brightness": 255,
|
||||||
|
"transition": 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mode": "single"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trigger Types
|
||||||
|
|
||||||
|
```json
|
||||||
|
// Time-based trigger
|
||||||
|
{
|
||||||
|
"platform": "time",
|
||||||
|
"at": "07:00:00"
|
||||||
|
}
|
||||||
|
|
||||||
|
// State-based trigger
|
||||||
|
{
|
||||||
|
"platform": "state",
|
||||||
|
"entity_id": "binary_sensor.motion",
|
||||||
|
"to": "on"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event-based trigger
|
||||||
|
{
|
||||||
|
"platform": "event",
|
||||||
|
"event_type": "custom_event"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numeric state trigger
|
||||||
|
{
|
||||||
|
"platform": "numeric_state",
|
||||||
|
"entity_id": "sensor.temperature",
|
||||||
|
"above": 25
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Condition Types
|
||||||
|
|
||||||
|
```json
|
||||||
|
// Time condition
|
||||||
|
{
|
||||||
|
"condition": "time",
|
||||||
|
"after": "07:00:00",
|
||||||
|
"before": "22:00:00"
|
||||||
|
}
|
||||||
|
|
||||||
|
// State condition
|
||||||
|
{
|
||||||
|
"condition": "state",
|
||||||
|
"entity_id": "device_tracker.phone",
|
||||||
|
"state": "home"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Numeric state condition
|
||||||
|
{
|
||||||
|
"condition": "numeric_state",
|
||||||
|
"entity_id": "sensor.temperature",
|
||||||
|
"below": 25
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Action Types
|
||||||
|
|
||||||
|
```json
|
||||||
|
// Service call action
|
||||||
|
{
|
||||||
|
"service": "light.turn_on",
|
||||||
|
"target": {
|
||||||
|
"entity_id": "light.bedroom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay action
|
||||||
|
{
|
||||||
|
"delay": "00:00:30"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scene activation
|
||||||
|
{
|
||||||
|
"scene": "scene.evening_mode"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conditional action
|
||||||
|
{
|
||||||
|
"choose": [
|
||||||
|
{
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"condition": "state",
|
||||||
|
"entity_id": "sun.sun",
|
||||||
|
"state": "below_horizon"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sequence": [
|
||||||
|
{
|
||||||
|
"service": "light.turn_on",
|
||||||
|
"target": {
|
||||||
|
"entity_id": "light.living_room"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Create New Automation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/automations', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"alias": "Morning Routine",
|
||||||
|
"description": "Turn on lights in the morning",
|
||||||
|
"trigger": [
|
||||||
|
{
|
||||||
|
"platform": "time",
|
||||||
|
"at": "07:00:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"action": [
|
||||||
|
{
|
||||||
|
"service": "light.turn_on",
|
||||||
|
"target": {
|
||||||
|
"entity_id": "light.bedroom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Existing Automation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/automations/morning_routine', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"alias": "Morning Routine",
|
||||||
|
"trigger": [
|
||||||
|
{
|
||||||
|
"platform": "time",
|
||||||
|
"at": "07:30:00" // Updated time
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"action": [
|
||||||
|
{
|
||||||
|
"service": "light.turn_on",
|
||||||
|
"target": {
|
||||||
|
"entity_id": "light.bedroom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"automation": {
|
||||||
|
"id": "created_automation_id",
|
||||||
|
// Full automation configuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validation Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"valid": true,
|
||||||
|
"warnings": [
|
||||||
|
"No conditions specified"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Error Codes
|
||||||
|
|
||||||
|
- `404`: Automation not found
|
||||||
|
- `401`: Unauthorized
|
||||||
|
- `400`: Invalid configuration
|
||||||
|
- `409`: Automation creation/update failed
|
||||||
|
|
||||||
|
### Error Response Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Error description",
|
||||||
|
"error_code": "ERROR_CODE",
|
||||||
|
"validation_errors": [
|
||||||
|
{
|
||||||
|
"path": "trigger[0].platform",
|
||||||
|
"message": "Invalid trigger platform"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. Always validate configurations before saving
|
||||||
|
2. Use descriptive aliases and descriptions
|
||||||
|
3. Group related automations
|
||||||
|
4. Test automations in a safe environment
|
||||||
|
5. Document automation dependencies
|
||||||
|
6. Use variables for reusable values
|
||||||
|
7. Implement proper error handling
|
||||||
|
8. Consider automation modes carefully
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Automation Management](automation.md)
|
||||||
|
- [Event Subscription](../events/subscribe-events.md)
|
||||||
|
- [Scene Management](../history-state/scene.md)
|
||||||
211
docs/tools/automation/automation.md
Normal file
211
docs/tools/automation/automation.md
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
# Automation Management Tool
|
||||||
|
|
||||||
|
The Automation Management tool provides functionality to manage and control Home Assistant automations.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- List all automations
|
||||||
|
- Get automation details
|
||||||
|
- Toggle automation state (enable/disable)
|
||||||
|
- Trigger automations manually
|
||||||
|
- Monitor automation execution
|
||||||
|
- View automation history
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
GET /api/automations
|
||||||
|
GET /api/automations/{automation_id}
|
||||||
|
POST /api/automations/{automation_id}/toggle
|
||||||
|
POST /api/automations/{automation_id}/trigger
|
||||||
|
GET /api/automations/{automation_id}/history
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// List automations
|
||||||
|
{
|
||||||
|
"type": "get_automations"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle automation
|
||||||
|
{
|
||||||
|
"type": "toggle_automation",
|
||||||
|
"automation_id": "required_automation_id"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger automation
|
||||||
|
{
|
||||||
|
"type": "trigger_automation",
|
||||||
|
"automation_id": "required_automation_id",
|
||||||
|
"variables": {
|
||||||
|
// Optional variables
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### List All Automations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/automations', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const automations = await response.json();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Toggle Automation State
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/automations/morning_routine/toggle', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trigger Automation Manually
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/automations/morning_routine/trigger', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"variables": {
|
||||||
|
"brightness": 100,
|
||||||
|
"temperature": 22
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
### Automation List Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"automations": [
|
||||||
|
{
|
||||||
|
"id": "automation_id",
|
||||||
|
"name": "Automation Name",
|
||||||
|
"enabled": true,
|
||||||
|
"last_triggered": "2024-02-05T12:00:00Z",
|
||||||
|
"trigger_count": 42
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automation Details Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"automation": {
|
||||||
|
"id": "automation_id",
|
||||||
|
"name": "Automation Name",
|
||||||
|
"enabled": true,
|
||||||
|
"triggers": [
|
||||||
|
{
|
||||||
|
"platform": "time",
|
||||||
|
"at": "07:00:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"conditions": [],
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"service": "light.turn_on",
|
||||||
|
"target": {
|
||||||
|
"entity_id": "light.bedroom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mode": "single",
|
||||||
|
"max": 10,
|
||||||
|
"last_triggered": "2024-02-05T12:00:00Z",
|
||||||
|
"trigger_count": 42
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Automation History Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"history": [
|
||||||
|
{
|
||||||
|
"timestamp": "2024-02-05T12:00:00Z",
|
||||||
|
"trigger": {
|
||||||
|
"platform": "time",
|
||||||
|
"at": "07:00:00"
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"user_id": "user_123",
|
||||||
|
"variables": {}
|
||||||
|
},
|
||||||
|
"result": "success"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Error Codes
|
||||||
|
|
||||||
|
- `404`: Automation not found
|
||||||
|
- `401`: Unauthorized
|
||||||
|
- `400`: Invalid request
|
||||||
|
- `409`: Automation execution failed
|
||||||
|
|
||||||
|
### Error Response Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Error description",
|
||||||
|
"error_code": "ERROR_CODE"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
- Default limit: 50 requests per 15 minutes
|
||||||
|
- Configurable through environment variables:
|
||||||
|
- `AUTOMATION_RATE_LIMIT`
|
||||||
|
- `AUTOMATION_RATE_WINDOW`
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. Monitor automation execution history
|
||||||
|
2. Use descriptive automation names
|
||||||
|
3. Implement proper error handling
|
||||||
|
4. Cache automation configurations when possible
|
||||||
|
5. Handle rate limiting gracefully
|
||||||
|
6. Test automations before enabling
|
||||||
|
7. Use variables for flexible automation behavior
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Automation Configuration](automation-config.md)
|
||||||
|
- [Event Subscription](../events/subscribe-events.md)
|
||||||
|
- [Device Control](../device-management/control.md)
|
||||||
195
docs/tools/device-management/control.md
Normal file
195
docs/tools/device-management/control.md
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
# Device Control Tool
|
||||||
|
|
||||||
|
The Device Control tool provides functionality to control various types of devices in your Home Assistant instance.
|
||||||
|
|
||||||
|
## Supported Device Types
|
||||||
|
|
||||||
|
- Lights
|
||||||
|
- Switches
|
||||||
|
- Covers
|
||||||
|
- Climate devices
|
||||||
|
- Media players
|
||||||
|
- And more...
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
POST /api/devices/{device_id}/control
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
"type": "control_device",
|
||||||
|
"device_id": "required_device_id",
|
||||||
|
"domain": "required_domain",
|
||||||
|
"service": "required_service",
|
||||||
|
"data": {
|
||||||
|
// Service-specific data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Domain-Specific Commands
|
||||||
|
|
||||||
|
### Lights
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Turn on/off
|
||||||
|
POST /api/devices/light/{device_id}/control
|
||||||
|
{
|
||||||
|
"service": "turn_on", // or "turn_off"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set brightness
|
||||||
|
{
|
||||||
|
"service": "turn_on",
|
||||||
|
"data": {
|
||||||
|
"brightness": 255 // 0-255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set color
|
||||||
|
{
|
||||||
|
"service": "turn_on",
|
||||||
|
"data": {
|
||||||
|
"rgb_color": [255, 0, 0] // Red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Covers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Open/close
|
||||||
|
POST /api/devices/cover/{device_id}/control
|
||||||
|
{
|
||||||
|
"service": "open_cover", // or "close_cover"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set position
|
||||||
|
{
|
||||||
|
"service": "set_cover_position",
|
||||||
|
"data": {
|
||||||
|
"position": 50 // 0-100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Climate
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Set temperature
|
||||||
|
POST /api/devices/climate/{device_id}/control
|
||||||
|
{
|
||||||
|
"service": "set_temperature",
|
||||||
|
"data": {
|
||||||
|
"temperature": 22.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set mode
|
||||||
|
{
|
||||||
|
"service": "set_hvac_mode",
|
||||||
|
"data": {
|
||||||
|
"hvac_mode": "heat" // heat, cool, auto, off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Control Light Brightness
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/devices/light/living_room/control', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"service": "turn_on",
|
||||||
|
"data": {
|
||||||
|
"brightness": 128
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Control Cover Position
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/devices/cover/bedroom/control', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"service": "set_cover_position",
|
||||||
|
"data": {
|
||||||
|
"position": 75
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"state": "on",
|
||||||
|
"attributes": {
|
||||||
|
// Updated device attributes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Error description",
|
||||||
|
"error_code": "ERROR_CODE"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Error Codes
|
||||||
|
|
||||||
|
- `404`: Device not found
|
||||||
|
- `401`: Unauthorized
|
||||||
|
- `400`: Invalid service or parameters
|
||||||
|
- `409`: Device unavailable or offline
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
- Default limit: 100 requests per 15 minutes
|
||||||
|
- Configurable through environment variables:
|
||||||
|
- `DEVICE_CONTROL_RATE_LIMIT`
|
||||||
|
- `DEVICE_CONTROL_RATE_WINDOW`
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. Validate device availability before sending commands
|
||||||
|
2. Implement proper error handling
|
||||||
|
3. Use appropriate retry strategies for failed commands
|
||||||
|
4. Cache device capabilities when possible
|
||||||
|
5. Handle rate limiting gracefully
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [List Devices](list-devices.md)
|
||||||
|
- [Device History](../history-state/history.md)
|
||||||
|
- [Event Subscription](../events/subscribe-events.md)
|
||||||
139
docs/tools/device-management/list-devices.md
Normal file
139
docs/tools/device-management/list-devices.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# List Devices Tool
|
||||||
|
|
||||||
|
The List Devices tool provides functionality to retrieve and manage device information from your Home Assistant instance.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- List all available Home Assistant devices
|
||||||
|
- Group devices by domain
|
||||||
|
- Get device states and attributes
|
||||||
|
- Filter devices by various criteria
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
GET /api/devices
|
||||||
|
GET /api/devices/{domain}
|
||||||
|
GET /api/devices/{device_id}/state
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// List all devices
|
||||||
|
{
|
||||||
|
"type": "list_devices",
|
||||||
|
"domain": "optional_domain"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get device state
|
||||||
|
{
|
||||||
|
"type": "get_device_state",
|
||||||
|
"device_id": "required_device_id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
#### List All Devices
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/devices', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const devices = await response.json();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Devices by Domain
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/devices/light', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const lightDevices = await response.json();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
### Device List Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"id": "device_id",
|
||||||
|
"name": "Device Name",
|
||||||
|
"domain": "light",
|
||||||
|
"state": "on",
|
||||||
|
"attributes": {
|
||||||
|
"brightness": 255,
|
||||||
|
"color_temp": 370
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Device State Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"state": "on",
|
||||||
|
"attributes": {
|
||||||
|
"brightness": 255,
|
||||||
|
"color_temp": 370
|
||||||
|
},
|
||||||
|
"last_changed": "2024-02-05T12:00:00Z",
|
||||||
|
"last_updated": "2024-02-05T12:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Error Codes
|
||||||
|
|
||||||
|
- `404`: Device not found
|
||||||
|
- `401`: Unauthorized
|
||||||
|
- `400`: Invalid request parameters
|
||||||
|
|
||||||
|
### Error Response Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Error description",
|
||||||
|
"error_code": "ERROR_CODE"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
- Default limit: 100 requests per 15 minutes
|
||||||
|
- Configurable through environment variables:
|
||||||
|
- `DEVICE_LIST_RATE_LIMIT`
|
||||||
|
- `DEVICE_LIST_RATE_WINDOW`
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. Cache device lists when possible
|
||||||
|
2. Use domain filtering for better performance
|
||||||
|
3. Implement proper error handling
|
||||||
|
4. Handle rate limiting gracefully
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Device Control](control.md)
|
||||||
|
- [Device History](../history-state/history.md)
|
||||||
|
- [Event Subscription](../events/subscribe-events.md)
|
||||||
251
docs/tools/events/sse-stats.md
Normal file
251
docs/tools/events/sse-stats.md
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
# SSE Statistics Tool
|
||||||
|
|
||||||
|
The SSE Statistics tool provides functionality to monitor and analyze Server-Sent Events (SSE) connections and performance in your Home Assistant MCP instance.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Monitor active SSE connections
|
||||||
|
- Track connection statistics
|
||||||
|
- Analyze event delivery
|
||||||
|
- Monitor resource usage
|
||||||
|
- Connection management
|
||||||
|
- Performance metrics
|
||||||
|
- Historical data
|
||||||
|
- Alert configuration
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
GET /api/sse/stats
|
||||||
|
GET /api/sse/connections
|
||||||
|
GET /api/sse/connections/{connection_id}
|
||||||
|
GET /api/sse/metrics
|
||||||
|
GET /api/sse/history
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get SSE stats
|
||||||
|
{
|
||||||
|
"type": "get_sse_stats"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get connection details
|
||||||
|
{
|
||||||
|
"type": "get_sse_connection",
|
||||||
|
"connection_id": "required_connection_id"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get performance metrics
|
||||||
|
{
|
||||||
|
"type": "get_sse_metrics",
|
||||||
|
"period": "1h|24h|7d|30d"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Get Current Statistics
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/sse/stats', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const stats = await response.json();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Connection Details
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/sse/connections/conn_123', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const connection = await response.json();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Performance Metrics
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/sse/metrics?period=24h', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const metrics = await response.json();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
### Statistics Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"active_connections": 42,
|
||||||
|
"total_events_sent": 12345,
|
||||||
|
"events_per_second": 5.2,
|
||||||
|
"memory_usage": 128974848,
|
||||||
|
"cpu_usage": 2.5,
|
||||||
|
"uptime": "PT24H",
|
||||||
|
"event_backlog": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection Details Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"connection": {
|
||||||
|
"id": "conn_123",
|
||||||
|
"client_id": "client_456",
|
||||||
|
"user_id": "user_789",
|
||||||
|
"connected_at": "2024-02-05T12:00:00Z",
|
||||||
|
"last_event_at": "2024-02-05T12:05:00Z",
|
||||||
|
"events_sent": 150,
|
||||||
|
"subscriptions": [
|
||||||
|
{
|
||||||
|
"event_type": "state_changed",
|
||||||
|
"entity_id": "light.living_room"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"state": "active",
|
||||||
|
"ip_address": "192.168.1.100",
|
||||||
|
"user_agent": "Mozilla/5.0 ..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Metrics Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"metrics": {
|
||||||
|
"connections": {
|
||||||
|
"current": 42,
|
||||||
|
"max": 100,
|
||||||
|
"average": 35.5
|
||||||
|
},
|
||||||
|
"events": {
|
||||||
|
"total": 12345,
|
||||||
|
"rate": {
|
||||||
|
"current": 5.2,
|
||||||
|
"max": 15.0,
|
||||||
|
"average": 4.8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"latency": {
|
||||||
|
"p50": 15,
|
||||||
|
"p95": 45,
|
||||||
|
"p99": 100
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"memory": {
|
||||||
|
"current": 128974848,
|
||||||
|
"max": 536870912
|
||||||
|
},
|
||||||
|
"cpu": {
|
||||||
|
"current": 2.5,
|
||||||
|
"max": 10.0,
|
||||||
|
"average": 3.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"period": "24h",
|
||||||
|
"timestamp": "2024-02-05T12:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Error Codes
|
||||||
|
|
||||||
|
- `404`: Connection not found
|
||||||
|
- `401`: Unauthorized
|
||||||
|
- `400`: Invalid request parameters
|
||||||
|
- `503`: Service overloaded
|
||||||
|
|
||||||
|
### Error Response Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Error description",
|
||||||
|
"error_code": "ERROR_CODE"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring Metrics
|
||||||
|
|
||||||
|
### Connection Metrics
|
||||||
|
- Active connections
|
||||||
|
- Connection duration
|
||||||
|
- Connection state
|
||||||
|
- Client information
|
||||||
|
- Geographic distribution
|
||||||
|
- Protocol version
|
||||||
|
|
||||||
|
### Event Metrics
|
||||||
|
- Events per second
|
||||||
|
- Event types distribution
|
||||||
|
- Delivery success rate
|
||||||
|
- Event latency
|
||||||
|
- Queue size
|
||||||
|
- Backlog size
|
||||||
|
|
||||||
|
### Resource Metrics
|
||||||
|
- Memory usage
|
||||||
|
- CPU usage
|
||||||
|
- Network bandwidth
|
||||||
|
- Disk I/O
|
||||||
|
- Connection pool status
|
||||||
|
- Thread pool status
|
||||||
|
|
||||||
|
## Alert Thresholds
|
||||||
|
|
||||||
|
- Connection limits
|
||||||
|
- Event rate limits
|
||||||
|
- Resource usage limits
|
||||||
|
- Latency thresholds
|
||||||
|
- Error rate thresholds
|
||||||
|
- Backlog thresholds
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. Monitor connection health
|
||||||
|
2. Track resource usage
|
||||||
|
3. Set up alerts
|
||||||
|
4. Analyze usage patterns
|
||||||
|
5. Optimize performance
|
||||||
|
6. Plan capacity
|
||||||
|
7. Implement failover
|
||||||
|
8. Regular maintenance
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
- Connection pooling
|
||||||
|
- Event batching
|
||||||
|
- Resource throttling
|
||||||
|
- Load balancing
|
||||||
|
- Cache optimization
|
||||||
|
- Connection cleanup
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Event Subscription](subscribe-events.md)
|
||||||
|
- [Device Control](../device-management/control.md)
|
||||||
|
- [Automation Management](../automation/automation.md)
|
||||||
253
docs/tools/events/subscribe-events.md
Normal file
253
docs/tools/events/subscribe-events.md
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
# Event Subscription Tool
|
||||||
|
|
||||||
|
The Event Subscription tool provides functionality to subscribe to and monitor real-time events from your Home Assistant instance.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Subscribe to Home Assistant events
|
||||||
|
- Monitor specific entities
|
||||||
|
- Domain-based monitoring
|
||||||
|
- Event filtering
|
||||||
|
- Real-time updates
|
||||||
|
- Event history
|
||||||
|
- Custom event handling
|
||||||
|
- Connection management
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
POST /api/events/subscribe
|
||||||
|
DELETE /api/events/unsubscribe
|
||||||
|
GET /api/events/subscriptions
|
||||||
|
GET /api/events/history
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Subscribe to events
|
||||||
|
{
|
||||||
|
"type": "subscribe_events",
|
||||||
|
"event_type": "optional_event_type",
|
||||||
|
"entity_id": "optional_entity_id",
|
||||||
|
"domain": "optional_domain"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe from events
|
||||||
|
{
|
||||||
|
"type": "unsubscribe_events",
|
||||||
|
"subscription_id": "required_subscription_id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server-Sent Events (SSE)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
GET /api/events/stream?event_type=state_changed&entity_id=light.living_room
|
||||||
|
```
|
||||||
|
|
||||||
|
## Event Types
|
||||||
|
|
||||||
|
- `state_changed`: Entity state changes
|
||||||
|
- `automation_triggered`: Automation executions
|
||||||
|
- `scene_activated`: Scene activations
|
||||||
|
- `device_registered`: New device registrations
|
||||||
|
- `service_registered`: New service registrations
|
||||||
|
- `homeassistant_start`: System startup
|
||||||
|
- `homeassistant_stop`: System shutdown
|
||||||
|
- Custom events
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Subscribe to All State Changes
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/events/subscribe', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"event_type": "state_changed"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitor Specific Entity
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/events/subscribe', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"event_type": "state_changed",
|
||||||
|
"entity_id": "light.living_room"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Domain-Based Monitoring
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/events/subscribe', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"event_type": "state_changed",
|
||||||
|
"domain": "light"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSE Connection Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const eventSource = new EventSource(
|
||||||
|
'http://your-ha-mcp/api/events/stream?event_type=state_changed&entity_id=light.living_room',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
eventSource.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
console.log('Event received:', data);
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = (error) => {
|
||||||
|
console.error('SSE error:', error);
|
||||||
|
eventSource.close();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
### Subscription Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"subscription_id": "sub_123",
|
||||||
|
"event_type": "state_changed",
|
||||||
|
"entity_id": "light.living_room",
|
||||||
|
"created_at": "2024-02-05T12:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Message Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event_type": "state_changed",
|
||||||
|
"entity_id": "light.living_room",
|
||||||
|
"data": {
|
||||||
|
"old_state": {
|
||||||
|
"state": "off",
|
||||||
|
"attributes": {},
|
||||||
|
"last_changed": "2024-02-05T11:55:00Z"
|
||||||
|
},
|
||||||
|
"new_state": {
|
||||||
|
"state": "on",
|
||||||
|
"attributes": {
|
||||||
|
"brightness": 255
|
||||||
|
},
|
||||||
|
"last_changed": "2024-02-05T12:00:00Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"origin": "LOCAL",
|
||||||
|
"time_fired": "2024-02-05T12:00:00Z",
|
||||||
|
"context": {
|
||||||
|
"id": "context_123",
|
||||||
|
"parent_id": null,
|
||||||
|
"user_id": "user_123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Subscriptions List Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"subscriptions": [
|
||||||
|
{
|
||||||
|
"id": "sub_123",
|
||||||
|
"event_type": "state_changed",
|
||||||
|
"entity_id": "light.living_room",
|
||||||
|
"created_at": "2024-02-05T12:00:00Z",
|
||||||
|
"last_event": "2024-02-05T12:05:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Error Codes
|
||||||
|
|
||||||
|
- `404`: Event type not found
|
||||||
|
- `401`: Unauthorized
|
||||||
|
- `400`: Invalid subscription parameters
|
||||||
|
- `409`: Subscription already exists
|
||||||
|
- `429`: Too many subscriptions
|
||||||
|
|
||||||
|
### Error Response Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Error description",
|
||||||
|
"error_code": "ERROR_CODE"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
- Default limits:
|
||||||
|
- Maximum subscriptions: 100 per client
|
||||||
|
- Maximum event rate: 1000 events per minute
|
||||||
|
- Configurable through environment variables:
|
||||||
|
- `EVENT_SUB_MAX_SUBSCRIPTIONS`
|
||||||
|
- `EVENT_SUB_RATE_LIMIT`
|
||||||
|
- `EVENT_SUB_RATE_WINDOW`
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. Use specific event types when possible
|
||||||
|
2. Implement proper error handling
|
||||||
|
3. Handle connection interruptions
|
||||||
|
4. Process events asynchronously
|
||||||
|
5. Implement backoff strategies
|
||||||
|
6. Monitor subscription health
|
||||||
|
7. Clean up unused subscriptions
|
||||||
|
8. Handle rate limiting gracefully
|
||||||
|
|
||||||
|
## Connection Management
|
||||||
|
|
||||||
|
- Implement heartbeat monitoring
|
||||||
|
- Use reconnection strategies
|
||||||
|
- Handle connection timeouts
|
||||||
|
- Monitor connection quality
|
||||||
|
- Implement fallback mechanisms
|
||||||
|
- Clean up resources properly
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [SSE Statistics](sse-stats.md)
|
||||||
|
- [Device Control](../device-management/control.md)
|
||||||
|
- [Automation Management](../automation/automation.md)
|
||||||
167
docs/tools/history-state/history.md
Normal file
167
docs/tools/history-state/history.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# Device History Tool
|
||||||
|
|
||||||
|
The Device History tool allows you to retrieve historical state information for devices in your Home Assistant instance.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Fetch device state history
|
||||||
|
- Filter by time range
|
||||||
|
- Get significant changes
|
||||||
|
- Aggregate data by time periods
|
||||||
|
- Export historical data
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
GET /api/history/{device_id}
|
||||||
|
GET /api/history/{device_id}/period/{start_time}
|
||||||
|
GET /api/history/{device_id}/period/{start_time}/{end_time}
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
"type": "get_history",
|
||||||
|
"device_id": "required_device_id",
|
||||||
|
"start_time": "optional_iso_timestamp",
|
||||||
|
"end_time": "optional_iso_timestamp",
|
||||||
|
"significant_changes_only": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Query Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|------|-------------|
|
||||||
|
| `start_time` | ISO timestamp | Start of the period to fetch history for |
|
||||||
|
| `end_time` | ISO timestamp | End of the period to fetch history for |
|
||||||
|
| `significant_changes_only` | boolean | Only return significant state changes |
|
||||||
|
| `minimal_response` | boolean | Return minimal state information |
|
||||||
|
| `no_attributes` | boolean | Exclude attribute data from response |
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Get Recent History
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/history/light.living_room', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const history = await response.json();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get History for Specific Period
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const startTime = '2024-02-01T00:00:00Z';
|
||||||
|
const endTime = '2024-02-02T00:00:00Z';
|
||||||
|
const response = await fetch(
|
||||||
|
`http://your-ha-mcp/api/history/light.living_room/period/${startTime}/${endTime}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const history = await response.json();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
### History Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"history": [
|
||||||
|
{
|
||||||
|
"state": "on",
|
||||||
|
"attributes": {
|
||||||
|
"brightness": 255
|
||||||
|
},
|
||||||
|
"last_changed": "2024-02-05T12:00:00Z",
|
||||||
|
"last_updated": "2024-02-05T12:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"state": "off",
|
||||||
|
"last_changed": "2024-02-05T13:00:00Z",
|
||||||
|
"last_updated": "2024-02-05T13:00:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Aggregated History Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"aggregates": {
|
||||||
|
"daily": [
|
||||||
|
{
|
||||||
|
"date": "2024-02-05",
|
||||||
|
"on_time": "PT5H30M",
|
||||||
|
"off_time": "PT18H30M",
|
||||||
|
"changes": 10
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Error Codes
|
||||||
|
|
||||||
|
- `404`: Device not found
|
||||||
|
- `401`: Unauthorized
|
||||||
|
- `400`: Invalid parameters
|
||||||
|
- `416`: Time range too large
|
||||||
|
|
||||||
|
### Error Response Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Error description",
|
||||||
|
"error_code": "ERROR_CODE"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
- Default limit: 50 requests per 15 minutes
|
||||||
|
- Configurable through environment variables:
|
||||||
|
- `HISTORY_RATE_LIMIT`
|
||||||
|
- `HISTORY_RATE_WINDOW`
|
||||||
|
|
||||||
|
## Data Retention
|
||||||
|
|
||||||
|
- Default retention period: 30 days
|
||||||
|
- Configurable through environment variables:
|
||||||
|
- `HISTORY_RETENTION_DAYS`
|
||||||
|
- Older data may be automatically aggregated
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. Use appropriate time ranges to avoid large responses
|
||||||
|
2. Enable `significant_changes_only` for better performance
|
||||||
|
3. Use `minimal_response` when full state data isn't needed
|
||||||
|
4. Implement proper error handling
|
||||||
|
5. Cache frequently accessed historical data
|
||||||
|
6. Handle rate limiting gracefully
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [List Devices](../device-management/list-devices.md)
|
||||||
|
- [Device Control](../device-management/control.md)
|
||||||
|
- [Scene Management](scene.md)
|
||||||
215
docs/tools/history-state/scene.md
Normal file
215
docs/tools/history-state/scene.md
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
# Scene Management Tool
|
||||||
|
|
||||||
|
The Scene Management tool provides functionality to manage and control scenes in your Home Assistant instance.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- List available scenes
|
||||||
|
- Activate scenes
|
||||||
|
- Create new scenes
|
||||||
|
- Update existing scenes
|
||||||
|
- Delete scenes
|
||||||
|
- Get scene state information
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
GET /api/scenes
|
||||||
|
GET /api/scenes/{scene_id}
|
||||||
|
POST /api/scenes/{scene_id}/activate
|
||||||
|
POST /api/scenes
|
||||||
|
PUT /api/scenes/{scene_id}
|
||||||
|
DELETE /api/scenes/{scene_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// List scenes
|
||||||
|
{
|
||||||
|
"type": "get_scenes"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate scene
|
||||||
|
{
|
||||||
|
"type": "activate_scene",
|
||||||
|
"scene_id": "required_scene_id"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create/Update scene
|
||||||
|
{
|
||||||
|
"type": "create_scene",
|
||||||
|
"scene": {
|
||||||
|
"name": "required_scene_name",
|
||||||
|
"entities": {
|
||||||
|
// Entity states
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scene Configuration
|
||||||
|
|
||||||
|
### Scene Definition
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Movie Night",
|
||||||
|
"entities": {
|
||||||
|
"light.living_room": {
|
||||||
|
"state": "on",
|
||||||
|
"brightness": 50,
|
||||||
|
"color_temp": 2700
|
||||||
|
},
|
||||||
|
"cover.living_room": {
|
||||||
|
"state": "closed"
|
||||||
|
},
|
||||||
|
"media_player.tv": {
|
||||||
|
"state": "on",
|
||||||
|
"source": "HDMI 1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### List All Scenes
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/scenes', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const scenes = await response.json();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Activate a Scene
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/scenes/movie_night/activate', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a New Scene
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/scenes', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"name": "Movie Night",
|
||||||
|
"entities": {
|
||||||
|
"light.living_room": {
|
||||||
|
"state": "on",
|
||||||
|
"brightness": 50
|
||||||
|
},
|
||||||
|
"cover.living_room": {
|
||||||
|
"state": "closed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
### Scene List Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"scenes": [
|
||||||
|
{
|
||||||
|
"id": "scene_id",
|
||||||
|
"name": "Scene Name",
|
||||||
|
"entities": {
|
||||||
|
// Entity configurations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scene Activation Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"scene_id": "activated_scene_id",
|
||||||
|
"status": "activated",
|
||||||
|
"timestamp": "2024-02-05T12:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Error Codes
|
||||||
|
|
||||||
|
- `404`: Scene not found
|
||||||
|
- `401`: Unauthorized
|
||||||
|
- `400`: Invalid scene configuration
|
||||||
|
- `409`: Scene activation failed
|
||||||
|
|
||||||
|
### Error Response Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Error description",
|
||||||
|
"error_code": "ERROR_CODE"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
- Default limit: 50 requests per 15 minutes
|
||||||
|
- Configurable through environment variables:
|
||||||
|
- `SCENE_RATE_LIMIT`
|
||||||
|
- `SCENE_RATE_WINDOW`
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. Validate entity availability before creating scenes
|
||||||
|
2. Use meaningful scene names
|
||||||
|
3. Group related entities in scenes
|
||||||
|
4. Implement proper error handling
|
||||||
|
5. Cache scene configurations when possible
|
||||||
|
6. Handle rate limiting gracefully
|
||||||
|
|
||||||
|
## Scene Transitions
|
||||||
|
|
||||||
|
Scenes can include transition settings for smooth state changes:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Sunset Mode",
|
||||||
|
"entities": {
|
||||||
|
"light.living_room": {
|
||||||
|
"state": "on",
|
||||||
|
"brightness": 128,
|
||||||
|
"transition": 5 // 5 seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Device Control](../device-management/control.md)
|
||||||
|
- [Device History](history.md)
|
||||||
|
- [Automation Management](../automation/automation.md)
|
||||||
249
docs/tools/notifications/notify.md
Normal file
249
docs/tools/notifications/notify.md
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
# Notification Tool
|
||||||
|
|
||||||
|
The Notification tool provides functionality to send notifications through various services in your Home Assistant instance.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Send notifications
|
||||||
|
- Support for multiple notification services
|
||||||
|
- Custom notification data
|
||||||
|
- Rich media support
|
||||||
|
- Notification templates
|
||||||
|
- Delivery tracking
|
||||||
|
- Priority levels
|
||||||
|
- Notification groups
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### REST API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
POST /api/notify
|
||||||
|
POST /api/notify/{service_id}
|
||||||
|
GET /api/notify/services
|
||||||
|
GET /api/notify/history
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Send notification
|
||||||
|
{
|
||||||
|
"type": "send_notification",
|
||||||
|
"service": "required_service_id",
|
||||||
|
"message": "required_message",
|
||||||
|
"title": "optional_title",
|
||||||
|
"data": {
|
||||||
|
// Service-specific data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get notification services
|
||||||
|
{
|
||||||
|
"type": "get_notification_services"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Services
|
||||||
|
|
||||||
|
- Mobile App
|
||||||
|
- Email
|
||||||
|
- SMS
|
||||||
|
- Telegram
|
||||||
|
- Discord
|
||||||
|
- Slack
|
||||||
|
- Push Notifications
|
||||||
|
- Custom Services
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Basic Notification
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/notify/mobile_app', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"message": "Motion detected in living room",
|
||||||
|
"title": "Security Alert"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rich Notification
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/notify/mobile_app', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"message": "Motion detected in living room",
|
||||||
|
"title": "Security Alert",
|
||||||
|
"data": {
|
||||||
|
"image": "https://your-camera-snapshot.jpg",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"action": "view_camera",
|
||||||
|
"title": "View Camera"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"action": "dismiss",
|
||||||
|
"title": "Dismiss"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"priority": "high",
|
||||||
|
"ttl": 3600,
|
||||||
|
"group": "security"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service-Specific Example (Telegram)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const response = await fetch('http://your-ha-mcp/api/notify/telegram', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer your_access_token',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"message": "Temperature is too high!",
|
||||||
|
"title": "Climate Alert",
|
||||||
|
"data": {
|
||||||
|
"parse_mode": "markdown",
|
||||||
|
"inline_keyboard": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"text": "Turn On AC",
|
||||||
|
"callback_data": "turn_on_ac"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"notification_id": "notification_123",
|
||||||
|
"status": "sent",
|
||||||
|
"timestamp": "2024-02-05T12:00:00Z",
|
||||||
|
"service": "mobile_app"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Services List Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"id": "mobile_app",
|
||||||
|
"name": "Mobile App",
|
||||||
|
"enabled": true,
|
||||||
|
"features": [
|
||||||
|
"actions",
|
||||||
|
"images",
|
||||||
|
"sound"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Notification History Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"history": [
|
||||||
|
{
|
||||||
|
"id": "notification_123",
|
||||||
|
"service": "mobile_app",
|
||||||
|
"message": "Motion detected",
|
||||||
|
"title": "Security Alert",
|
||||||
|
"timestamp": "2024-02-05T12:00:00Z",
|
||||||
|
"status": "delivered"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Error Codes
|
||||||
|
|
||||||
|
- `404`: Service not found
|
||||||
|
- `401`: Unauthorized
|
||||||
|
- `400`: Invalid request
|
||||||
|
- `408`: Delivery timeout
|
||||||
|
- `422`: Invalid notification data
|
||||||
|
|
||||||
|
### Error Response Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "Error description",
|
||||||
|
"error_code": "ERROR_CODE"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
- Default limit: 100 notifications per hour
|
||||||
|
- Configurable through environment variables:
|
||||||
|
- `NOTIFY_RATE_LIMIT`
|
||||||
|
- `NOTIFY_RATE_WINDOW`
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. Use appropriate priority levels
|
||||||
|
2. Group related notifications
|
||||||
|
3. Include relevant context
|
||||||
|
4. Implement proper error handling
|
||||||
|
5. Use templates for consistency
|
||||||
|
6. Consider time zones
|
||||||
|
7. Respect user preferences
|
||||||
|
8. Handle rate limiting gracefully
|
||||||
|
|
||||||
|
## Notification Templates
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Template example
|
||||||
|
{
|
||||||
|
"template": "security_alert",
|
||||||
|
"data": {
|
||||||
|
"location": "living_room",
|
||||||
|
"event_type": "motion",
|
||||||
|
"timestamp": "2024-02-05T12:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Event Subscription](../events/subscribe-events.md)
|
||||||
|
- [Device Control](../device-management/control.md)
|
||||||
|
- [Automation Management](../automation/automation.md)
|
||||||
@@ -6,36 +6,36 @@ This section documents all available tools in the Home Assistant MCP.
|
|||||||
|
|
||||||
### Device Management
|
### Device Management
|
||||||
|
|
||||||
1. [List Devices](./list-devices.md)
|
1. [List Devices](device-management/list-devices.md)
|
||||||
- List all available Home Assistant devices
|
- List all available Home Assistant devices
|
||||||
- Group devices by domain
|
- Group devices by domain
|
||||||
- Get device states and attributes
|
- Get device states and attributes
|
||||||
|
|
||||||
2. [Device Control](./control.md)
|
2. [Device Control](device-management/control.md)
|
||||||
- Control various device types
|
- Control various device types
|
||||||
- Support for lights, switches, covers, climate devices
|
- Support for lights, switches, covers, climate devices
|
||||||
- Domain-specific commands and parameters
|
- Domain-specific commands and parameters
|
||||||
|
|
||||||
### History and State
|
### History and State
|
||||||
|
|
||||||
1. [History](./history.md)
|
1. [History](history-state/history.md)
|
||||||
- Fetch device state history
|
- Fetch device state history
|
||||||
- Filter by time range
|
- Filter by time range
|
||||||
- Get significant changes
|
- Get significant changes
|
||||||
|
|
||||||
2. [Scene Management](./scene.md)
|
2. [Scene Management](history-state/scene.md)
|
||||||
- List available scenes
|
- List available scenes
|
||||||
- Activate scenes
|
- Activate scenes
|
||||||
- Scene state information
|
- Scene state information
|
||||||
|
|
||||||
### Automation
|
### Automation
|
||||||
|
|
||||||
1. [Automation Management](./automation.md)
|
1. [Automation Management](automation/automation.md)
|
||||||
- List automations
|
- List automations
|
||||||
- Toggle automation state
|
- Toggle automation state
|
||||||
- Trigger automations manually
|
- Trigger automations manually
|
||||||
|
|
||||||
2. [Automation Configuration](./automation-config.md)
|
2. [Automation Configuration](automation/automation-config.md)
|
||||||
- Create new automations
|
- Create new automations
|
||||||
- Update existing automations
|
- Update existing automations
|
||||||
- Delete automations
|
- Delete automations
|
||||||
@@ -43,32 +43,32 @@ This section documents all available tools in the Home Assistant MCP.
|
|||||||
|
|
||||||
### Add-ons and Packages
|
### Add-ons and Packages
|
||||||
|
|
||||||
1. [Add-on Management](./addon.md)
|
1. [Add-on Management](addons-packages/addon.md)
|
||||||
- List available add-ons
|
- List available add-ons
|
||||||
- Install/uninstall add-ons
|
- Install/uninstall add-ons
|
||||||
- Start/stop/restart add-ons
|
- Start/stop/restart add-ons
|
||||||
- Get add-on information
|
- Get add-on information
|
||||||
|
|
||||||
2. [Package Management](./package.md)
|
2. [Package Management](addons-packages/package.md)
|
||||||
- Manage HACS packages
|
- Manage HACS packages
|
||||||
- Install/update/remove packages
|
- Install/update/remove packages
|
||||||
- List available packages by category
|
- List available packages by category
|
||||||
|
|
||||||
### Notifications
|
### Notifications
|
||||||
|
|
||||||
1. [Notify](./notify.md)
|
1. [Notify](notifications/notify.md)
|
||||||
- Send notifications
|
- Send notifications
|
||||||
- Support for multiple notification services
|
- Support for multiple notification services
|
||||||
- Custom notification data
|
- Custom notification data
|
||||||
|
|
||||||
### Real-time Events
|
### Real-time Events
|
||||||
|
|
||||||
1. [Event Subscription](./subscribe-events.md)
|
1. [Event Subscription](events/subscribe-events.md)
|
||||||
- Subscribe to Home Assistant events
|
- Subscribe to Home Assistant events
|
||||||
- Monitor specific entities
|
- Monitor specific entities
|
||||||
- Domain-based monitoring
|
- Domain-based monitoring
|
||||||
|
|
||||||
2. [SSE Statistics](./sse-stats.md)
|
2. [SSE Statistics](events/sse-stats.md)
|
||||||
- Get SSE connection statistics
|
- Get SSE connection statistics
|
||||||
- Monitor active subscriptions
|
- Monitor active subscriptions
|
||||||
- Connection management
|
- Connection management
|
||||||
|
|||||||
@@ -312,4 +312,63 @@ tar -czf mcp-backup-$(date +%Y%m%d).tar.gz \
|
|||||||
.env \
|
.env \
|
||||||
config/ \
|
config/ \
|
||||||
data/
|
data/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
### General Questions
|
||||||
|
|
||||||
|
#### Q: What is MCP Server?
|
||||||
|
A: MCP Server is a bridge between Home Assistant and Language Learning Models, enabling natural language control and automation of your smart home devices.
|
||||||
|
|
||||||
|
#### Q: What are the system requirements?
|
||||||
|
A: MCP Server requires:
|
||||||
|
- Node.js 16 or higher
|
||||||
|
- Home Assistant instance
|
||||||
|
- 1GB RAM minimum
|
||||||
|
- 1GB disk space
|
||||||
|
|
||||||
|
#### Q: How do I update MCP Server?
|
||||||
|
A: For Docker installation:
|
||||||
|
```bash
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
For manual installation:
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
bun install
|
||||||
|
bun run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Questions
|
||||||
|
|
||||||
|
#### Q: Can I use MCP Server with any Home Assistant instance?
|
||||||
|
A: Yes, MCP Server works with any Home Assistant instance that has the REST API enabled and a valid long-lived access token.
|
||||||
|
|
||||||
|
#### Q: Does MCP Server support all Home Assistant integrations?
|
||||||
|
A: MCP Server supports all Home Assistant devices and services that are accessible via the REST API.
|
||||||
|
|
||||||
|
### Security Questions
|
||||||
|
|
||||||
|
#### Q: Is my Home Assistant token secure?
|
||||||
|
A: Yes, your Home Assistant token is stored securely and only used for authenticated communication between MCP Server and your Home Assistant instance.
|
||||||
|
|
||||||
|
#### Q: Can I use MCP Server remotely?
|
||||||
|
A: Yes, but we recommend using a secure connection (HTTPS) and proper authentication when exposing MCP Server to the internet.
|
||||||
|
|
||||||
|
### Troubleshooting Questions
|
||||||
|
|
||||||
|
#### Q: Why are my device states not updating?
|
||||||
|
A: Check:
|
||||||
|
1. Home Assistant connection
|
||||||
|
2. WebSocket connection status
|
||||||
|
3. Device availability in Home Assistant
|
||||||
|
4. Network connectivity
|
||||||
|
|
||||||
|
#### Q: Why are my commands not working?
|
||||||
|
A: Verify:
|
||||||
|
1. Command syntax
|
||||||
|
2. Device availability
|
||||||
|
3. User permissions
|
||||||
|
4. Home Assistant API access
|
||||||
33
mkdocs.yml
33
mkdocs.yml
@@ -82,15 +82,48 @@ plugins:
|
|||||||
nav:
|
nav:
|
||||||
- Home: index.md
|
- Home: index.md
|
||||||
- Getting Started:
|
- Getting Started:
|
||||||
|
- Overview: getting-started.md
|
||||||
- Installation: getting-started/installation.md
|
- Installation: getting-started/installation.md
|
||||||
|
- Configuration: getting-started/configuration.md
|
||||||
|
- Docker Setup: getting-started/docker.md
|
||||||
- Quick Start: getting-started/quickstart.md
|
- Quick Start: getting-started/quickstart.md
|
||||||
|
- Usage: usage.md
|
||||||
- API Reference:
|
- API Reference:
|
||||||
- Overview: api/index.md
|
- Overview: api/index.md
|
||||||
|
- Core API: api.md
|
||||||
- SSE API: api/sse.md
|
- SSE API: api/sse.md
|
||||||
- Core Functions: api/core.md
|
- Core Functions: api/core.md
|
||||||
|
- Tools:
|
||||||
|
- Overview: tools/tools.md
|
||||||
|
- Device Management:
|
||||||
|
- List Devices: tools/device-management/list-devices.md
|
||||||
|
- Device Control: tools/device-management/control.md
|
||||||
|
- History & State:
|
||||||
|
- History: tools/history-state/history.md
|
||||||
|
- Scene Management: tools/history-state/scene.md
|
||||||
|
- Automation:
|
||||||
|
- Automation Management: tools/automation/automation.md
|
||||||
|
- Automation Configuration: tools/automation/automation-config.md
|
||||||
|
- Add-ons & Packages:
|
||||||
|
- Add-on Management: tools/addons-packages/addon.md
|
||||||
|
- Package Management: tools/addons-packages/package.md
|
||||||
|
- Notifications:
|
||||||
|
- Notify: tools/notifications/notify.md
|
||||||
|
- Events:
|
||||||
|
- Event Subscription: tools/events/subscribe-events.md
|
||||||
|
- SSE Statistics: tools/events/sse-stats.md
|
||||||
|
- Development:
|
||||||
|
- Overview: development/development.md
|
||||||
|
- Best Practices: development/best-practices.md
|
||||||
|
- Interfaces: development/interfaces.md
|
||||||
|
- Tool Development: development/tools.md
|
||||||
|
- Testing Guide: testing.md
|
||||||
- Architecture: architecture.md
|
- Architecture: architecture.md
|
||||||
- Contributing: contributing.md
|
- Contributing: contributing.md
|
||||||
- Troubleshooting: troubleshooting.md
|
- Troubleshooting: troubleshooting.md
|
||||||
|
- Examples:
|
||||||
|
- Overview: examples/index.md
|
||||||
|
- Roadmap: roadmap.md
|
||||||
|
|
||||||
extra:
|
extra:
|
||||||
social:
|
social:
|
||||||
|
|||||||
@@ -30,11 +30,13 @@
|
|||||||
"@types/node": "^20.11.24",
|
"@types/node": "^20.11.24",
|
||||||
"@types/sanitize-html": "^2.9.5",
|
"@types/sanitize-html": "^2.9.5",
|
||||||
"@types/ws": "^8.5.10",
|
"@types/ws": "^8.5.10",
|
||||||
|
"@xmldom/xmldom": "^0.9.7",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"elysia": "^1.2.11",
|
"elysia": "^1.2.11",
|
||||||
"helmet": "^7.1.0",
|
"helmet": "^7.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
|
"openai": "^4.82.0",
|
||||||
"sanitize-html": "^2.11.0",
|
"sanitize-html": "^2.11.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
|
|||||||
12
src/utils/helpers.ts
Normal file
12
src/utils/helpers.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Formats a tool call response into a standardized structure
|
||||||
|
* @param obj The object to format
|
||||||
|
* @param isError Whether this is an error response
|
||||||
|
* @returns Formatted response object
|
||||||
|
*/
|
||||||
|
export const formatToolCall = (obj: any, isError: boolean = false) => {
|
||||||
|
const text = obj === undefined ? 'undefined' : JSON.stringify(obj, null, 2);
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text, isError }],
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user