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
|
||||
__pycache__/
|
||||
*.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
|
||||
graph TD
|
||||
subgraph Client
|
||||
A[Client Application<br>(Web / Mobile / Voice)]
|
||||
A["Client Application (Web/Mobile/Voice)"]
|
||||
end
|
||||
subgraph CDN
|
||||
B[CDN / Cache]
|
||||
B["CDN / Cache"]
|
||||
end
|
||||
subgraph Server
|
||||
C[Bun Native Server]
|
||||
E[NLP Engine &<br>Language Processing Module]
|
||||
C["Bun Native Server"]
|
||||
E["NLP Engine & Language Processing Module"]
|
||||
end
|
||||
subgraph Integration
|
||||
D[Home Assistant<br>(Devices, Lights, Thermostats)]
|
||||
D["Home Assistant (Devices, Lights, Thermostats)"]
|
||||
end
|
||||
|
||||
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';
|
||||
|
||||
// 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 }],
|
||||
};
|
||||
};
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { formatToolCall } from "../src/utils/helpers";
|
||||
|
||||
describe('helpers', () => {
|
||||
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 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 result = formatToolCall(testObj, true);
|
||||
|
||||
@@ -35,7 +29,7 @@ describe('helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty objects', () => {
|
||||
test('should handle empty objects', () => {
|
||||
const 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 { LiteMCP } from 'litemcp';
|
||||
import { get_hass } from '../src/hass/index.js';
|
||||
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
|
||||
import type { Mock } from "bun:test";
|
||||
import type { WebSocket } from 'ws';
|
||||
import type { LiteMCP } from 'litemcp';
|
||||
|
||||
// Load test environment variables with defaults
|
||||
const TEST_HASS_HOST = process.env.TEST_HASS_HOST || 'http://localhost:8123';
|
||||
const TEST_HASS_TOKEN = process.env.TEST_HASS_TOKEN || 'test_token';
|
||||
const TEST_HASS_SOCKET_URL = process.env.TEST_HASS_SOCKET_URL || 'ws://localhost:8123/api/websocket';
|
||||
// Extend the global scope
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var mockResponse: Response;
|
||||
}
|
||||
|
||||
// Set environment variables for testing
|
||||
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
|
||||
// Types
|
||||
interface Tool {
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -44,30 +17,18 @@ interface Tool {
|
||||
execute: (params: Record<string, unknown>) => Promise<unknown>;
|
||||
}
|
||||
|
||||
type MockFunction<T = any> = jest.Mock<Promise<T>, any[]>;
|
||||
|
||||
interface MockLiteMCPInstance {
|
||||
addTool: ReturnType<typeof jest.fn>;
|
||||
start: ReturnType<typeof jest.fn>;
|
||||
addTool: Mock<(tool: Tool) => void>;
|
||||
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 {
|
||||
light: {
|
||||
turn_on: jest.Mock;
|
||||
turn_off: jest.Mock;
|
||||
turn_on: Mock<() => Promise<{ success: boolean }>>;
|
||||
turn_off: Mock<() => Promise<{ success: boolean }>>;
|
||||
};
|
||||
climate: {
|
||||
set_temperature: jest.Mock;
|
||||
set_temperature: Mock<() => Promise<{ success: boolean }>>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,21 +36,6 @@ interface MockHassInstance {
|
||||
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 {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
@@ -103,99 +49,212 @@ interface TestResponse {
|
||||
new_automation_id?: string;
|
||||
}
|
||||
|
||||
type WebSocketEventMap = {
|
||||
message: MessageEvent;
|
||||
open: Event;
|
||||
close: Event;
|
||||
error: Event;
|
||||
// Test configuration
|
||||
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;
|
||||
|
||||
// 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;
|
||||
type WebSocketMessageListener = (event: MessageEvent) => void;
|
||||
const mockServices: MockServices = {
|
||||
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 {
|
||||
addEventListener: jest.Mock;
|
||||
removeEventListener: jest.Mock;
|
||||
send: jest.Mock;
|
||||
close: jest.Mock;
|
||||
readyState: number;
|
||||
binaryType: 'blob' | 'arraybuffer';
|
||||
bufferedAmount: number;
|
||||
extensions: string;
|
||||
protocol: string;
|
||||
url: string;
|
||||
onopen: WebSocketEventListener | null;
|
||||
onerror: WebSocketEventListener | null;
|
||||
onclose: WebSocketEventListener | null;
|
||||
onmessage: WebSocketMessageListener | null;
|
||||
CONNECTING: number;
|
||||
OPEN: number;
|
||||
CLOSING: number;
|
||||
CLOSED: number;
|
||||
// Mock WebSocket
|
||||
class MockWebSocket implements Partial<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 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);
|
||||
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 => ({
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
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
|
||||
// Create fetch mock with implementation
|
||||
let mockFetch = mock(() => {
|
||||
return Promise.resolve(new Response());
|
||||
});
|
||||
|
||||
// Override globals
|
||||
globalThis.fetch = mockFetch;
|
||||
// Use type assertion to handle WebSocket compatibility
|
||||
globalThis.WebSocket = MockWebSocket as any;
|
||||
|
||||
describe('Home Assistant MCP Server', () => {
|
||||
let mockHass: MockHassInstance;
|
||||
let liteMcpInstance: MockLiteMCPInstance;
|
||||
let addToolCalls: Array<[Tool]>;
|
||||
let addToolCalls: Tool[];
|
||||
|
||||
beforeEach(async () => {
|
||||
mockHass = {
|
||||
services: mockServices
|
||||
};
|
||||
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
mockFetch.mockClear();
|
||||
// Reset mocks
|
||||
mockLiteMCPInstance.addTool.mock.calls = [];
|
||||
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
|
||||
await import('../src/index.js');
|
||||
|
||||
// Mock WebSocket
|
||||
const mockWs = createMockWebSocket();
|
||||
(global as any).WebSocket = jest.fn(() => mockWs);
|
||||
|
||||
// Get the mock instance
|
||||
liteMcpInstance = mockLiteMCPInstance;
|
||||
addToolCalls = liteMcpInstance.addTool.mock.calls as Array<[Tool]>;
|
||||
addToolCalls = mockLiteMCPInstance.addTool.mock.calls.map(call => call.args[0]);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const hass = await get_hass();
|
||||
expect(hass).toBeDefined();
|
||||
expect(hass.services).toBeDefined();
|
||||
expect(typeof hass.services.light.turn_on).toBe('function');
|
||||
test('should connect to Home Assistant', async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
// Verify connection
|
||||
expect(mockFetch.mock.calls.length).toBeGreaterThan(0);
|
||||
expect(mockLiteMCPInstance.start.mock.calls.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should reuse the same instance on subsequent calls', async () => {
|
||||
const firstInstance = await get_hass();
|
||||
const secondInstance = await get_hass();
|
||||
expect(firstInstance).toBe(secondInstance);
|
||||
test('should handle connection errors', async () => {
|
||||
// Setup error response
|
||||
mockFetch = mock(() => Promise.reject(new Error('Connection failed')));
|
||||
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', () => {
|
||||
@@ -220,7 +279,7 @@ describe('Home Assistant MCP Server', () => {
|
||||
} as Response);
|
||||
|
||||
// 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();
|
||||
|
||||
if (!listDevicesTool) {
|
||||
@@ -251,7 +310,7 @@ describe('Home Assistant MCP Server', () => {
|
||||
mockFetch.mockRejectedValueOnce(new Error('Network error'));
|
||||
|
||||
// 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();
|
||||
|
||||
if (!listDevicesTool) {
|
||||
@@ -276,7 +335,7 @@ describe('Home Assistant MCP Server', () => {
|
||||
} as Response);
|
||||
|
||||
// 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();
|
||||
|
||||
if (!controlTool) {
|
||||
@@ -296,11 +355,11 @@ describe('Home Assistant MCP Server', () => {
|
||||
|
||||
// Verify the fetch call
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`${TEST_HASS_HOST}/api/services/light/turn_on`,
|
||||
`${TEST_CONFIG.HASS_HOST}/api/services/light/turn_on`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
||||
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
@@ -313,7 +372,7 @@ describe('Home Assistant MCP Server', () => {
|
||||
|
||||
it('should handle unsupported domains', async () => {
|
||||
// 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();
|
||||
|
||||
if (!controlTool) {
|
||||
@@ -339,7 +398,7 @@ describe('Home Assistant MCP Server', () => {
|
||||
} as Response);
|
||||
|
||||
// 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();
|
||||
|
||||
if (!controlTool) {
|
||||
@@ -365,7 +424,7 @@ describe('Home Assistant MCP Server', () => {
|
||||
} as Response);
|
||||
|
||||
// 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();
|
||||
|
||||
if (!controlTool) {
|
||||
@@ -387,11 +446,11 @@ describe('Home Assistant MCP Server', () => {
|
||||
|
||||
// Verify the fetch call
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`${TEST_HASS_HOST}/api/services/climate/set_temperature`,
|
||||
`${TEST_CONFIG.HASS_HOST}/api/services/climate/set_temperature`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
||||
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
@@ -412,7 +471,7 @@ describe('Home Assistant MCP Server', () => {
|
||||
} as Response);
|
||||
|
||||
// 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();
|
||||
|
||||
if (!controlTool) {
|
||||
@@ -432,11 +491,11 @@ describe('Home Assistant MCP Server', () => {
|
||||
|
||||
// Verify the fetch call
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`${TEST_HASS_HOST}/api/services/cover/set_position`,
|
||||
`${TEST_CONFIG.HASS_HOST}/api/services/cover/set_position`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
||||
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
@@ -449,36 +508,29 @@ describe('Home Assistant MCP Server', () => {
|
||||
});
|
||||
|
||||
describe('get_history tool', () => {
|
||||
it('should successfully fetch history', async () => {
|
||||
test('should successfully fetch history', async () => {
|
||||
const mockHistory = [
|
||||
{
|
||||
entity_id: 'light.living_room',
|
||||
state: 'on',
|
||||
last_changed: '2024-01-01T00:00:00Z',
|
||||
attributes: { brightness: 255 }
|
||||
},
|
||||
{
|
||||
entity_id: 'light.living_room',
|
||||
state: 'off',
|
||||
last_changed: '2024-01-01T01:00:00Z',
|
||||
attributes: { brightness: 0 }
|
||||
}
|
||||
];
|
||||
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockHistory
|
||||
} as Response);
|
||||
// Setup response for this test
|
||||
mockFetch = mock(() => Promise.resolve(new Response(
|
||||
JSON.stringify(mockHistory)
|
||||
)));
|
||||
globalThis.fetch = mockFetch;
|
||||
|
||||
// Get the tool registration
|
||||
const historyTool = addToolCalls.find(call => call[0].name === 'get_history')?.[0];
|
||||
const historyTool = addToolCalls.find(call => call.name === 'get_history');
|
||||
expect(historyTool).toBeDefined();
|
||||
|
||||
if (!historyTool) {
|
||||
throw new Error('get_history tool not found');
|
||||
}
|
||||
|
||||
// Execute the tool
|
||||
const result = (await historyTool.execute({
|
||||
entity_id: 'light.living_room',
|
||||
start_time: '2024-01-01T00:00:00Z',
|
||||
@@ -491,29 +543,36 @@ describe('Home Assistant MCP Server', () => {
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.history).toEqual(mockHistory);
|
||||
|
||||
// Verify the fetch call
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/api/history/period/2024-01-01T00:00:00Z?'),
|
||||
expect.objectContaining({
|
||||
headers: {
|
||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
);
|
||||
// Verify the fetch call was made with correct URL and parameters
|
||||
const calls = mockFetch.mock.calls;
|
||||
expect(calls.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify query parameters
|
||||
const url = mockFetch.mock.calls[0][0] as string;
|
||||
const queryParams = new URL(url).searchParams;
|
||||
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 firstCall = calls[0];
|
||||
if (!firstCall?.args) {
|
||||
throw new Error('No fetch calls recorded');
|
||||
}
|
||||
|
||||
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 () => {
|
||||
mockFetch.mockRejectedValueOnce(new Error('Network error'));
|
||||
test('should handle fetch errors', async () => {
|
||||
// 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();
|
||||
|
||||
if (!historyTool) {
|
||||
@@ -555,7 +614,7 @@ describe('Home Assistant MCP Server', () => {
|
||||
json: async () => mockScenes
|
||||
} as Response);
|
||||
|
||||
const sceneTool = addToolCalls.find(call => call[0].name === 'scene')?.[0];
|
||||
const sceneTool = addToolCalls.find(call => call.name === 'scene');
|
||||
expect(sceneTool).toBeDefined();
|
||||
|
||||
if (!sceneTool) {
|
||||
@@ -587,7 +646,7 @@ describe('Home Assistant MCP Server', () => {
|
||||
json: async () => ({})
|
||||
} as Response);
|
||||
|
||||
const sceneTool = addToolCalls.find(call => call[0].name === 'scene')?.[0];
|
||||
const sceneTool = addToolCalls.find(call => call.name === 'scene');
|
||||
expect(sceneTool).toBeDefined();
|
||||
|
||||
if (!sceneTool) {
|
||||
@@ -603,11 +662,11 @@ describe('Home Assistant MCP Server', () => {
|
||||
expect(result.message).toBe('Successfully activated scene scene.movie_time');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`${TEST_HASS_HOST}/api/services/scene/turn_on`,
|
||||
`${TEST_CONFIG.HASS_HOST}/api/services/scene/turn_on`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
||||
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
@@ -625,7 +684,7 @@ describe('Home Assistant MCP Server', () => {
|
||||
json: async () => ({})
|
||||
} as Response);
|
||||
|
||||
const notifyTool = addToolCalls.find(call => call[0].name === 'notify')?.[0];
|
||||
const notifyTool = addToolCalls.find(call => call.name === 'notify');
|
||||
expect(notifyTool).toBeDefined();
|
||||
|
||||
if (!notifyTool) {
|
||||
@@ -643,11 +702,11 @@ describe('Home Assistant MCP Server', () => {
|
||||
expect(result.message).toBe('Notification sent successfully');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`${TEST_HASS_HOST}/api/services/notify/mobile_app_phone`,
|
||||
`${TEST_CONFIG.HASS_HOST}/api/services/notify/mobile_app_phone`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
||||
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
@@ -660,12 +719,13 @@ describe('Home Assistant MCP Server', () => {
|
||||
});
|
||||
|
||||
it('should use default notification service when no target is specified', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({})
|
||||
} as Response);
|
||||
// Setup response for this test
|
||||
mockFetch = mock(() => Promise.resolve(new Response(
|
||||
JSON.stringify({})
|
||||
)));
|
||||
globalThis.fetch = mockFetch;
|
||||
|
||||
const notifyTool = addToolCalls.find(call => call[0].name === 'notify')?.[0];
|
||||
const notifyTool = addToolCalls.find(call => call.name === 'notify');
|
||||
expect(notifyTool).toBeDefined();
|
||||
|
||||
if (!notifyTool) {
|
||||
@@ -676,10 +736,11 @@ describe('Home Assistant MCP Server', () => {
|
||||
message: 'Test notification'
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`${TEST_HASS_HOST}/api/services/notify/notify`,
|
||||
expect.any(Object)
|
||||
);
|
||||
const calls = mockFetch.mock.calls;
|
||||
expect(calls.length).toBeGreaterThan(0);
|
||||
|
||||
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
|
||||
} as Response);
|
||||
|
||||
const automationTool = addToolCalls.find(call => call[0].name === 'automation')?.[0];
|
||||
const automationTool = addToolCalls.find(call => call.name === 'automation');
|
||||
expect(automationTool).toBeDefined();
|
||||
|
||||
if (!automationTool) {
|
||||
@@ -743,7 +804,7 @@ describe('Home Assistant MCP Server', () => {
|
||||
json: async () => ({})
|
||||
} as Response);
|
||||
|
||||
const automationTool = addToolCalls.find(call => call[0].name === 'automation')?.[0];
|
||||
const automationTool = addToolCalls.find(call => call.name === 'automation');
|
||||
expect(automationTool).toBeDefined();
|
||||
|
||||
if (!automationTool) {
|
||||
@@ -759,11 +820,11 @@ describe('Home Assistant MCP Server', () => {
|
||||
expect(result.message).toBe('Successfully toggled automation automation.morning_routine');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`${TEST_HASS_HOST}/api/services/automation/toggle`,
|
||||
`${TEST_CONFIG.HASS_HOST}/api/services/automation/toggle`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
||||
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
@@ -779,7 +840,7 @@ describe('Home Assistant MCP Server', () => {
|
||||
json: async () => ({})
|
||||
} as Response);
|
||||
|
||||
const automationTool = addToolCalls.find(call => call[0].name === 'automation')?.[0];
|
||||
const automationTool = addToolCalls.find(call => call.name === 'automation');
|
||||
expect(automationTool).toBeDefined();
|
||||
|
||||
if (!automationTool) {
|
||||
@@ -795,11 +856,11 @@ describe('Home Assistant MCP Server', () => {
|
||||
expect(result.message).toBe('Successfully triggered automation automation.morning_routine');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`${TEST_HASS_HOST}/api/services/automation/trigger`,
|
||||
`${TEST_CONFIG.HASS_HOST}/api/services/automation/trigger`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
||||
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
@@ -810,7 +871,7 @@ describe('Home Assistant MCP Server', () => {
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
if (!automationTool) {
|
||||
@@ -858,7 +919,7 @@ describe('Home Assistant MCP Server', () => {
|
||||
json: async () => mockAddons
|
||||
} as Response);
|
||||
|
||||
const addonTool = addToolCalls.find(call => call[0].name === 'addon')?.[0];
|
||||
const addonTool = addToolCalls.find(call => call.name === 'addon');
|
||||
expect(addonTool).toBeDefined();
|
||||
|
||||
if (!addonTool) {
|
||||
@@ -879,7 +940,7 @@ describe('Home Assistant MCP Server', () => {
|
||||
json: async () => ({ data: { state: 'installing' } })
|
||||
} as Response);
|
||||
|
||||
const addonTool = addToolCalls.find(call => call[0].name === 'addon')?.[0];
|
||||
const addonTool = addToolCalls.find(call => call.name === 'addon');
|
||||
expect(addonTool).toBeDefined();
|
||||
|
||||
if (!addonTool) {
|
||||
@@ -896,11 +957,11 @@ describe('Home Assistant MCP Server', () => {
|
||||
expect(result.message).toBe('Successfully installed add-on core_configurator');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`${TEST_HASS_HOST}/api/hassio/addons/core_configurator/install`,
|
||||
`${TEST_CONFIG.HASS_HOST}/api/hassio/addons/core_configurator/install`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
||||
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ version: '5.6.0' })
|
||||
@@ -931,7 +992,7 @@ describe('Home Assistant MCP Server', () => {
|
||||
json: async () => mockPackages
|
||||
} as Response);
|
||||
|
||||
const packageTool = addToolCalls.find(call => call[0].name === 'package')?.[0];
|
||||
const packageTool = addToolCalls.find(call => call.name === 'package');
|
||||
expect(packageTool).toBeDefined();
|
||||
|
||||
if (!packageTool) {
|
||||
@@ -953,7 +1014,7 @@ describe('Home Assistant MCP Server', () => {
|
||||
json: async () => ({})
|
||||
} as Response);
|
||||
|
||||
const packageTool = addToolCalls.find(call => call[0].name === 'package')?.[0];
|
||||
const packageTool = addToolCalls.find(call => call.name === 'package');
|
||||
expect(packageTool).toBeDefined();
|
||||
|
||||
if (!packageTool) {
|
||||
@@ -971,11 +1032,11 @@ describe('Home Assistant MCP Server', () => {
|
||||
expect(result.message).toBe('Successfully installed package hacs/integration');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`${TEST_HASS_HOST}/api/hacs/repository/install`,
|
||||
`${TEST_CONFIG.HASS_HOST}/api/hacs/repository/install`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
||||
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
@@ -1016,7 +1077,7 @@ describe('Home Assistant MCP Server', () => {
|
||||
json: async () => ({ automation_id: 'new_automation_1' })
|
||||
} 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();
|
||||
|
||||
if (!automationConfigTool) {
|
||||
@@ -1033,11 +1094,11 @@ describe('Home Assistant MCP Server', () => {
|
||||
expect(result.automation_id).toBe('new_automation_1');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`${TEST_HASS_HOST}/api/config/automation/config`,
|
||||
`${TEST_CONFIG.HASS_HOST}/api/config/automation/config`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
||||
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(mockAutomationConfig)
|
||||
@@ -1058,7 +1119,7 @@ describe('Home Assistant MCP Server', () => {
|
||||
json: async () => ({ automation_id: 'new_automation_2' })
|
||||
} 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();
|
||||
|
||||
if (!automationConfigTool) {
|
||||
@@ -1076,17 +1137,17 @@ describe('Home Assistant MCP Server', () => {
|
||||
|
||||
// Verify both API calls
|
||||
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)
|
||||
);
|
||||
|
||||
const duplicateConfig = { ...mockAutomationConfig, alias: 'Test Automation (Copy)' };
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
`${TEST_HASS_HOST}/api/config/automation/config`,
|
||||
`${TEST_CONFIG.HASS_HOST}/api/config/automation/config`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${TEST_HASS_TOKEN}`,
|
||||
Authorization: `Bearer ${TEST_CONFIG.HASS_TOKEN}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(duplicateConfig)
|
||||
@@ -1095,7 +1156,7 @@ describe('Home Assistant MCP Server', () => {
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
if (!automationConfigTool) {
|
||||
|
||||
@@ -1,61 +1,81 @@
|
||||
import { jest, describe, beforeEach, afterEach, it, expect } from '@jest/globals';
|
||||
import express from 'express';
|
||||
import { LiteMCP } from 'litemcp';
|
||||
import { logger } from '../src/utils/logger.js';
|
||||
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
|
||||
import type { Mock } from "bun:test";
|
||||
import type { Express, Application } from 'express';
|
||||
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
|
||||
jest.mock('express', () => {
|
||||
const mockApp = {
|
||||
use: jest.fn(),
|
||||
listen: jest.fn((port: number, callback: () => void) => {
|
||||
callback();
|
||||
return { close: jest.fn() };
|
||||
})
|
||||
};
|
||||
return jest.fn(() => mockApp);
|
||||
});
|
||||
const mockApp: MockApp = {
|
||||
use: mock(() => undefined),
|
||||
listen: mock((port: number, callback: () => void) => {
|
||||
callback();
|
||||
return { close: mock(() => undefined) };
|
||||
})
|
||||
};
|
||||
const mockExpress = mock(() => mockApp);
|
||||
|
||||
// Mock LiteMCP
|
||||
jest.mock('litemcp', () => ({
|
||||
LiteMCP: jest.fn(() => ({
|
||||
addTool: jest.fn(),
|
||||
start: jest.fn().mockImplementation(async () => { })
|
||||
}))
|
||||
}));
|
||||
// Mock LiteMCP instance
|
||||
const mockLiteMCPInstance: MockLiteMCPInstance = {
|
||||
addTool: mock(() => undefined),
|
||||
start: mock(() => Promise.resolve())
|
||||
};
|
||||
const mockLiteMCP = mock((name: string, version: string) => mockLiteMCPInstance);
|
||||
|
||||
// Mock logger
|
||||
jest.mock('../src/utils/logger.js', () => ({
|
||||
logger: {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn()
|
||||
}
|
||||
}));
|
||||
const mockLogger: MockLogger = {
|
||||
info: mock((message: string) => undefined),
|
||||
error: mock((message: string) => undefined),
|
||||
debug: mock((message: string) => undefined)
|
||||
};
|
||||
|
||||
describe('Server Initialization', () => {
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
let mockApp: ReturnType<typeof express>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Store original environment
|
||||
originalEnv = { ...process.env };
|
||||
|
||||
// Reset all mocks
|
||||
jest.clearAllMocks();
|
||||
// Setup mocks
|
||||
(globalThis as any).express = mockExpress;
|
||||
(globalThis as any).LiteMCP = mockLiteMCP;
|
||||
(globalThis as any).logger = mockLogger;
|
||||
|
||||
// Get the mock express app
|
||||
mockApp = express();
|
||||
// Reset all mocks
|
||||
mockApp.use.mockReset();
|
||||
mockApp.listen.mockReset();
|
||||
mockLogger.info.mockReset();
|
||||
mockLogger.error.mockReset();
|
||||
mockLogger.debug.mockReset();
|
||||
mockLiteMCP.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original environment
|
||||
process.env = originalEnv;
|
||||
|
||||
// Clear module cache to ensure fresh imports
|
||||
jest.resetModules();
|
||||
// Clean up mocks
|
||||
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
|
||||
process.env.PROCESSOR_TYPE = 'openai';
|
||||
|
||||
@@ -63,13 +83,15 @@ describe('Server Initialization', () => {
|
||||
await import('../src/index.js');
|
||||
|
||||
// Verify Express server was initialized
|
||||
expect(express).toHaveBeenCalled();
|
||||
expect(mockApp.use).toHaveBeenCalled();
|
||||
expect(mockApp.listen).toHaveBeenCalled();
|
||||
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Server is running on port'));
|
||||
expect(mockExpress.mock.calls.length).toBeGreaterThan(0);
|
||||
expect(mockApp.use.mock.calls.length).toBeGreaterThan(0);
|
||||
expect(mockApp.listen.mock.calls.length).toBeGreaterThan(0);
|
||||
|
||||
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
|
||||
process.env.PROCESSOR_TYPE = 'claude';
|
||||
|
||||
@@ -77,28 +99,38 @@ describe('Server Initialization', () => {
|
||||
await import('../src/index.js');
|
||||
|
||||
// Verify Express server was not initialized
|
||||
expect(express).not.toHaveBeenCalled();
|
||||
expect(mockApp.use).not.toHaveBeenCalled();
|
||||
expect(mockApp.listen).not.toHaveBeenCalled();
|
||||
expect(logger.info).toHaveBeenCalledWith('Running in Claude mode - Express server disabled');
|
||||
expect(mockExpress.mock.calls.length).toBe(0);
|
||||
expect(mockApp.use.mock.calls.length).toBe(0);
|
||||
expect(mockApp.listen.mock.calls.length).toBe(0);
|
||||
|
||||
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
|
||||
process.env.PROCESSOR_TYPE = 'openai';
|
||||
await import('../src/index.js');
|
||||
expect(LiteMCP).toHaveBeenCalledWith('home-assistant', expect.any(String));
|
||||
|
||||
// Reset modules
|
||||
jest.resetModules();
|
||||
expect(mockLiteMCP.mock.calls.length).toBeGreaterThan(0);
|
||||
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
|
||||
process.env.PROCESSOR_TYPE = 'claude';
|
||||
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
|
||||
delete process.env.PROCESSOR_TYPE;
|
||||
|
||||
@@ -106,9 +138,11 @@ describe('Server Initialization', () => {
|
||||
await import('../src/index.js');
|
||||
|
||||
// Verify Express server was initialized (default behavior)
|
||||
expect(express).toHaveBeenCalled();
|
||||
expect(mockApp.use).toHaveBeenCalled();
|
||||
expect(mockApp.listen).toHaveBeenCalled();
|
||||
expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Server is running on port'));
|
||||
expect(mockExpress.mock.calls.length).toBeGreaterThan(0);
|
||||
expect(mockApp.use.mock.calls.length).toBeGreaterThan(0);
|
||||
expect(mockApp.listen.mock.calls.length).toBeGreaterThan(0);
|
||||
|
||||
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": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
@@ -9,11 +9,13 @@
|
||||
"@types/node": "^20.11.24",
|
||||
"@types/sanitize-html": "^2.9.5",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@xmldom/xmldom": "^0.9.7",
|
||||
"dotenv": "^16.4.5",
|
||||
"elysia": "^1.2.11",
|
||||
"helmet": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"node-fetch": "^3.3.2",
|
||||
"openai": "^4.82.0",
|
||||
"sanitize-html": "^2.11.0",
|
||||
"typescript": "^5.3.3",
|
||||
"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-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/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=="],
|
||||
|
||||
"@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-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=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
@@ -233,6 +243,8 @@
|
||||
|
||||
"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-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-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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"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=="],
|
||||
@@ -531,6 +553,10 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"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-plugins \
|
||||
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
|
||||
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
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the wake word detection script
|
||||
# Copy the wake word detection script and audio setup script
|
||||
COPY wake_word_detector.py .
|
||||
COPY setup-audio.sh /setup-audio.sh
|
||||
RUN chmod +x /setup-audio.sh
|
||||
|
||||
# Set environment variables
|
||||
ENV WHISPER_MODEL_PATH=/models \
|
||||
WAKEWORD_MODEL_PATH=/models/wake_word \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
ASR_MODEL=base.en \
|
||||
ASR_MODEL_PATH=/models
|
||||
PULSE_SERVER=unix:/run/user/1000/pulse/native \
|
||||
HOME=/home/user
|
||||
|
||||
# Add resource limits to Python
|
||||
ENV PYTHONMALLOC=malloc \
|
||||
MALLOC_TRIM_THRESHOLD_=100000 \
|
||||
PYTHONDEVMODE=1
|
||||
# Run as the host user
|
||||
USER 1000:1000
|
||||
|
||||
# Add healthcheck
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
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"]
|
||||
# Start the application
|
||||
CMD ["/setup-audio.sh"]
|
||||
@@ -1,7 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Wait for PulseAudio to be ready
|
||||
sleep 2
|
||||
# Wait for PulseAudio socket to be available
|
||||
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
|
||||
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:
|
||||
|
||||
- **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.
|
||||
- **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
|
||||
|
||||
|
||||
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:
|
||||
|
||||
#### Smithery Configuration
|
||||
|
||||
The project includes a `smithery.yaml` configuration:
|
||||
|
||||
```yaml
|
||||
# Add smithery.yaml contents and explanation
|
||||
```
|
||||
|
||||
#### Installation Steps
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
1. [List Devices](./list-devices.md)
|
||||
1. [List Devices](device-management/list-devices.md)
|
||||
- List all available Home Assistant devices
|
||||
- Group devices by domain
|
||||
- Get device states and attributes
|
||||
|
||||
2. [Device Control](./control.md)
|
||||
2. [Device Control](device-management/control.md)
|
||||
- Control various device types
|
||||
- Support for lights, switches, covers, climate devices
|
||||
- Domain-specific commands and parameters
|
||||
|
||||
### History and State
|
||||
|
||||
1. [History](./history.md)
|
||||
1. [History](history-state/history.md)
|
||||
- Fetch device state history
|
||||
- Filter by time range
|
||||
- Get significant changes
|
||||
|
||||
2. [Scene Management](./scene.md)
|
||||
2. [Scene Management](history-state/scene.md)
|
||||
- List available scenes
|
||||
- Activate scenes
|
||||
- Scene state information
|
||||
|
||||
### Automation
|
||||
|
||||
1. [Automation Management](./automation.md)
|
||||
1. [Automation Management](automation/automation.md)
|
||||
- List automations
|
||||
- Toggle automation state
|
||||
- Trigger automations manually
|
||||
|
||||
2. [Automation Configuration](./automation-config.md)
|
||||
2. [Automation Configuration](automation/automation-config.md)
|
||||
- Create new automations
|
||||
- Update existing automations
|
||||
- Delete automations
|
||||
@@ -43,32 +43,32 @@ This section documents all available tools in the Home Assistant MCP.
|
||||
|
||||
### Add-ons and Packages
|
||||
|
||||
1. [Add-on Management](./addon.md)
|
||||
1. [Add-on Management](addons-packages/addon.md)
|
||||
- List available add-ons
|
||||
- Install/uninstall add-ons
|
||||
- Start/stop/restart add-ons
|
||||
- Get add-on information
|
||||
|
||||
2. [Package Management](./package.md)
|
||||
2. [Package Management](addons-packages/package.md)
|
||||
- Manage HACS packages
|
||||
- Install/update/remove packages
|
||||
- List available packages by category
|
||||
|
||||
### Notifications
|
||||
|
||||
1. [Notify](./notify.md)
|
||||
1. [Notify](notifications/notify.md)
|
||||
- Send notifications
|
||||
- Support for multiple notification services
|
||||
- Custom notification data
|
||||
|
||||
### Real-time Events
|
||||
|
||||
1. [Event Subscription](./subscribe-events.md)
|
||||
1. [Event Subscription](events/subscribe-events.md)
|
||||
- Subscribe to Home Assistant events
|
||||
- Monitor specific entities
|
||||
- Domain-based monitoring
|
||||
|
||||
2. [SSE Statistics](./sse-stats.md)
|
||||
2. [SSE Statistics](events/sse-stats.md)
|
||||
- Get SSE connection statistics
|
||||
- Monitor active subscriptions
|
||||
- Connection management
|
||||
|
||||
@@ -312,4 +312,63 @@ tar -czf mcp-backup-$(date +%Y%m%d).tar.gz \
|
||||
.env \
|
||||
config/ \
|
||||
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:
|
||||
- Home: index.md
|
||||
- Getting Started:
|
||||
- Overview: getting-started.md
|
||||
- Installation: getting-started/installation.md
|
||||
- Configuration: getting-started/configuration.md
|
||||
- Docker Setup: getting-started/docker.md
|
||||
- Quick Start: getting-started/quickstart.md
|
||||
- Usage: usage.md
|
||||
- API Reference:
|
||||
- Overview: api/index.md
|
||||
- Core API: api.md
|
||||
- SSE API: api/sse.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
|
||||
- Contributing: contributing.md
|
||||
- Troubleshooting: troubleshooting.md
|
||||
- Examples:
|
||||
- Overview: examples/index.md
|
||||
- Roadmap: roadmap.md
|
||||
|
||||
extra:
|
||||
social:
|
||||
|
||||
@@ -30,11 +30,13 @@
|
||||
"@types/node": "^20.11.24",
|
||||
"@types/sanitize-html": "^2.9.5",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@xmldom/xmldom": "^0.9.7",
|
||||
"dotenv": "^16.4.5",
|
||||
"elysia": "^1.2.11",
|
||||
"helmet": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"node-fetch": "^3.3.2",
|
||||
"openai": "^4.82.0",
|
||||
"sanitize-html": "^2.11.0",
|
||||
"typescript": "^5.3.3",
|
||||
"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