Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e96fa163cd | ||
|
|
cfef80e1e5 | ||
|
|
9b74a4354b | ||
|
|
fca193b5b2 | ||
|
|
cc9eede856 | ||
|
|
f0ff3d5e5a | ||
|
|
81d6dea7da | ||
|
|
1328bd1306 | ||
|
|
6fa88be433 | ||
|
|
2892f24030 | ||
|
|
1e3442db14 | ||
|
|
f74154d96f | ||
|
|
36d83e0a0e | ||
|
|
33defac76c | ||
|
|
4306a6866f | ||
|
|
039f6890a7 | ||
|
|
4fff318ea9 |
82
.github/workflows/deploy-docs.yml
vendored
82
.github/workflows/deploy-docs.yml
vendored
@@ -1,38 +1,76 @@
|
||||
name: Deploy MkDocs
|
||||
name: Deploy Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main # or master, depending on your default branch
|
||||
- main
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'mkdocs.yml'
|
||||
# Allow manual trigger
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install mkdocs-material
|
||||
pip install mkdocs-git-revision-date-localized-plugin
|
||||
pip install mkdocstrings[python]
|
||||
- name: Configure Git
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r docs/requirements.txt
|
||||
|
||||
- name: List mkdocs configuration
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
- name: Build docs
|
||||
run: mkdocs build
|
||||
echo "Current directory contents:"
|
||||
ls -la
|
||||
echo "MkDocs version:"
|
||||
mkdocs --version
|
||||
echo "MkDocs configuration:"
|
||||
cat mkdocs.yml
|
||||
|
||||
- name: Build documentation
|
||||
run: |
|
||||
mkdocs build --strict
|
||||
echo "Build output contents:"
|
||||
ls -la site/advanced-homeassistant-mcp
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./site/advanced-homeassistant-mcp
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
run: |
|
||||
git checkout --orphan gh-pages
|
||||
git rm -rf .
|
||||
mv site/* .
|
||||
rm -rf site
|
||||
git add .
|
||||
git commit -m "docs: Update documentation"
|
||||
git push origin gh-pages --force
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
32
.github/workflows/docs-deploy.yml
vendored
32
.github/workflows/docs-deploy.yml
vendored
@@ -1,32 +0,0 @@
|
||||
name: Deploy Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'mkdocs.yml'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
deploy-docs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install mkdocs-material
|
||||
pip install mkdocs
|
||||
|
||||
- name: Deploy documentation
|
||||
run: mkdocs gh-deploy --force
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -31,7 +31,7 @@ wheels/
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
.venv/
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
@@ -90,3 +90,12 @@ __pycache__/
|
||||
*$py.class
|
||||
|
||||
models/
|
||||
|
||||
*.code-workspace
|
||||
*.ttf
|
||||
*.otf
|
||||
*.woff
|
||||
*.woff2
|
||||
*.eot
|
||||
*.svg
|
||||
*.png
|
||||
86
README.md
86
README.md
@@ -12,12 +12,22 @@ MCP (Model Context Protocol) Server is a lightweight integration tool for Home A
|
||||
- 📡 WebSocket/Server-Sent Events (SSE) for state updates
|
||||
- 🤖 Simple automation rule management
|
||||
- 🔐 JWT-based authentication
|
||||
- 🎤 Real-time device control and monitoring
|
||||
- 🎤 Server-Sent Events (SSE) for live updates
|
||||
- 🎤 Comprehensive logging
|
||||
- 🎤 Optional speech features:
|
||||
- 🎤 Wake word detection ("hey jarvis", "ok google", "alexa")
|
||||
- 🎤 Speech-to-text using fast-whisper
|
||||
- 🎤 Multiple language support
|
||||
- 🎤 GPU acceleration support
|
||||
|
||||
## Prerequisites 📋
|
||||
|
||||
- 🚀 Bun runtime (v1.0.26+)
|
||||
- 🏡 Home Assistant instance
|
||||
- 🐳 Docker (optional, recommended for deployment)
|
||||
- 🐳 Docker (optional, recommended for deployment and speech features)
|
||||
- 🖥️ Node.js 18+ (optional, for speech features)
|
||||
- 🖥️ NVIDIA GPU with CUDA support (optional, for faster speech processing)
|
||||
|
||||
## Installation 🛠️
|
||||
|
||||
@@ -30,7 +40,7 @@ cd homeassistant-mcp
|
||||
|
||||
# Copy and edit environment configuration
|
||||
cp .env.example .env
|
||||
# Edit .env with your Home Assistant credentials
|
||||
# Edit .env with your Home Assistant credentials and speech features settings
|
||||
|
||||
# Build and start containers
|
||||
docker compose up -d --build
|
||||
@@ -79,33 +89,69 @@ ws.onmessage = (event) => {
|
||||
};
|
||||
```
|
||||
|
||||
## Current Limitations ⚠️
|
||||
## Speech Features (Optional)
|
||||
|
||||
- 🎙️ Basic voice command support (work in progress)
|
||||
- 🧠 Limited advanced NLP capabilities
|
||||
- 🔗 Minimal third-party device integration
|
||||
- 🐛 Early-stage error handling
|
||||
The MCP Server includes optional speech processing capabilities:
|
||||
|
||||
## Contributing 🤝
|
||||
### Prerequisites
|
||||
1. Docker installed and running
|
||||
2. NVIDIA GPU with CUDA support (optional)
|
||||
3. At least 4GB RAM (8GB+ recommended for larger models)
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch:
|
||||
### Setup
|
||||
|
||||
1. Enable speech features in your .env:
|
||||
```bash
|
||||
git checkout -b feature/your-feature
|
||||
ENABLE_SPEECH_FEATURES=true
|
||||
ENABLE_WAKE_WORD=true
|
||||
ENABLE_SPEECH_TO_TEXT=true
|
||||
WHISPER_MODEL_PATH=/models
|
||||
WHISPER_MODEL_TYPE=base
|
||||
```
|
||||
3. Make your changes
|
||||
4. Run tests:
|
||||
|
||||
2. Start the speech services:
|
||||
```bash
|
||||
bun test
|
||||
docker-compose up -d
|
||||
```
|
||||
5. Submit a pull request
|
||||
|
||||
## Roadmap 🗺️
|
||||
### Available Models
|
||||
|
||||
- 🎤 Enhance voice command processing
|
||||
- 🔌 Improve device compatibility
|
||||
- 🤖 Expand automation capabilities
|
||||
- 🛡️ Implement more robust error handling
|
||||
Choose a model based on your needs:
|
||||
- `tiny.en`: Fastest, basic accuracy
|
||||
- `base.en`: Good balance (recommended)
|
||||
- `small.en`: Better accuracy, slower
|
||||
- `medium.en`: High accuracy, resource intensive
|
||||
- `large-v2`: Best accuracy, very resource intensive
|
||||
|
||||
### Usage
|
||||
|
||||
1. Wake word detection listens for:
|
||||
- "hey jarvis"
|
||||
- "ok google"
|
||||
- "alexa"
|
||||
|
||||
2. After wake word detection:
|
||||
- Audio is automatically captured
|
||||
- Speech is transcribed
|
||||
- Commands are processed
|
||||
|
||||
3. Manual transcription is also available:
|
||||
```typescript
|
||||
const speech = speechService.getSpeechToText();
|
||||
const text = await speech.transcribe(audioBuffer);
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
See [Configuration Guide](docs/configuration.md) for detailed settings.
|
||||
|
||||
## API Documentation
|
||||
|
||||
See [API Documentation](docs/api/index.md) for available endpoints.
|
||||
|
||||
## Development
|
||||
|
||||
See [Development Guide](docs/development/index.md) for contribution guidelines.
|
||||
|
||||
## License 📄
|
||||
|
||||
|
||||
@@ -1,149 +1,149 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
|
||||
import { describe, expect, test, beforeEach, afterEach, mock, spyOn } from "bun:test";
|
||||
import type { Mock } from "bun:test";
|
||||
import type { Express, Application } from 'express';
|
||||
import type { Logger } from 'winston';
|
||||
import type { Elysia } from "elysia";
|
||||
|
||||
// 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
|
||||
const mockApp: MockApp = {
|
||||
use: mock(() => undefined),
|
||||
listen: mock((port: number, callback: () => void) => {
|
||||
callback();
|
||||
return { close: mock(() => undefined) };
|
||||
// Create mock instances
|
||||
const mockApp = {
|
||||
use: mock(() => mockApp),
|
||||
get: mock(() => mockApp),
|
||||
post: mock(() => mockApp),
|
||||
listen: mock((port: number, callback?: () => void) => {
|
||||
callback?.();
|
||||
return mockApp;
|
||||
})
|
||||
};
|
||||
const mockExpress = mock(() => mockApp);
|
||||
|
||||
// Mock LiteMCP instance
|
||||
const mockLiteMCPInstance: MockLiteMCPInstance = {
|
||||
addTool: mock(() => undefined),
|
||||
start: mock(() => Promise.resolve())
|
||||
// Create mock constructors
|
||||
const MockElysia = mock(() => mockApp);
|
||||
const mockCors = mock(() => (app: any) => app);
|
||||
const mockSwagger = mock(() => (app: any) => app);
|
||||
const mockSpeechService = {
|
||||
initialize: mock(() => Promise.resolve()),
|
||||
shutdown: mock(() => Promise.resolve())
|
||||
};
|
||||
const mockLiteMCP = mock((name: string, version: string) => mockLiteMCPInstance);
|
||||
|
||||
// Mock logger
|
||||
const mockLogger: MockLogger = {
|
||||
info: mock((message: string) => undefined),
|
||||
error: mock((message: string) => undefined),
|
||||
debug: mock((message: string) => undefined)
|
||||
// Mock the modules
|
||||
const mockModules = {
|
||||
Elysia: MockElysia,
|
||||
cors: mockCors,
|
||||
swagger: mockSwagger,
|
||||
speechService: mockSpeechService,
|
||||
config: mock(() => ({})),
|
||||
resolve: mock((...args: string[]) => args.join('/')),
|
||||
z: { object: mock(() => ({})), enum: mock(() => ({})) }
|
||||
};
|
||||
|
||||
// Mock module resolution
|
||||
const mockResolver = {
|
||||
resolve(specifier: string) {
|
||||
const mocks: Record<string, any> = {
|
||||
'elysia': { Elysia: mockModules.Elysia },
|
||||
'@elysiajs/cors': { cors: mockModules.cors },
|
||||
'@elysiajs/swagger': { swagger: mockModules.swagger },
|
||||
'../speech/index.js': { speechService: mockModules.speechService },
|
||||
'dotenv': { config: mockModules.config },
|
||||
'path': { resolve: mockModules.resolve },
|
||||
'zod': { z: mockModules.z }
|
||||
};
|
||||
return mocks[specifier] || {};
|
||||
}
|
||||
};
|
||||
|
||||
describe('Server Initialization', () => {
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
let consoleLog: Mock<typeof console.log>;
|
||||
let consoleError: Mock<typeof console.error>;
|
||||
let originalResolve: any;
|
||||
|
||||
beforeEach(() => {
|
||||
// Store original environment
|
||||
originalEnv = { ...process.env };
|
||||
|
||||
// Setup mocks
|
||||
(globalThis as any).express = mockExpress;
|
||||
(globalThis as any).LiteMCP = mockLiteMCP;
|
||||
(globalThis as any).logger = mockLogger;
|
||||
// Mock console methods
|
||||
consoleLog = mock(() => { });
|
||||
consoleError = mock(() => { });
|
||||
console.log = consoleLog;
|
||||
console.error = consoleError;
|
||||
|
||||
// Reset all mocks
|
||||
mockApp.use.mockReset();
|
||||
mockApp.listen.mockReset();
|
||||
mockLogger.info.mockReset();
|
||||
mockLogger.error.mockReset();
|
||||
mockLogger.debug.mockReset();
|
||||
mockLiteMCP.mockReset();
|
||||
for (const key in mockModules) {
|
||||
const module = mockModules[key as keyof typeof mockModules];
|
||||
if (typeof module === 'object' && module !== null) {
|
||||
Object.values(module).forEach(value => {
|
||||
if (typeof value === 'function' && 'mock' in value) {
|
||||
(value as Mock<any>).mockReset();
|
||||
}
|
||||
});
|
||||
} else if (typeof module === 'function' && 'mock' in module) {
|
||||
(module as Mock<any>).mockReset();
|
||||
}
|
||||
}
|
||||
|
||||
// Set default environment variables
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.PORT = '4000';
|
||||
|
||||
// Setup module resolution mock
|
||||
originalResolve = (globalThis as any).Bun?.resolveSync;
|
||||
(globalThis as any).Bun = {
|
||||
...(globalThis as any).Bun,
|
||||
resolveSync: (specifier: string) => mockResolver.resolve(specifier)
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original environment
|
||||
process.env = originalEnv;
|
||||
|
||||
// Clean up mocks
|
||||
delete (globalThis as any).express;
|
||||
delete (globalThis as any).LiteMCP;
|
||||
delete (globalThis as any).logger;
|
||||
// Restore module resolution
|
||||
if (originalResolve) {
|
||||
(globalThis as any).Bun.resolveSync = originalResolve;
|
||||
}
|
||||
});
|
||||
|
||||
test('should start Express server when not in Claude mode', async () => {
|
||||
// Set OpenAI mode
|
||||
process.env.PROCESSOR_TYPE = 'openai';
|
||||
test('should initialize server with middleware', async () => {
|
||||
// Import and initialize server
|
||||
const mod = await import('../src/index');
|
||||
|
||||
// Import the main module
|
||||
await import('../src/index.js');
|
||||
// Verify server initialization
|
||||
expect(MockElysia.mock.calls.length).toBe(1);
|
||||
expect(mockCors.mock.calls.length).toBe(1);
|
||||
expect(mockSwagger.mock.calls.length).toBe(1);
|
||||
|
||||
// Verify Express server was initialized
|
||||
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);
|
||||
// Verify console output
|
||||
const logCalls = consoleLog.mock.calls;
|
||||
expect(logCalls.some(call =>
|
||||
typeof call.args[0] === 'string' &&
|
||||
call.args[0].includes('Server is running on port')
|
||||
)).toBe(true);
|
||||
});
|
||||
|
||||
test('should not start Express server in Claude mode', async () => {
|
||||
// Set Claude mode
|
||||
process.env.PROCESSOR_TYPE = 'claude';
|
||||
test('should initialize speech service when enabled', async () => {
|
||||
// Enable speech service
|
||||
process.env.SPEECH_ENABLED = 'true';
|
||||
|
||||
// Import the main module
|
||||
await import('../src/index.js');
|
||||
// Import and initialize server
|
||||
const mod = await import('../src/index');
|
||||
|
||||
// Verify Express server was not initialized
|
||||
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');
|
||||
// Verify speech service initialization
|
||||
expect(mockSpeechService.initialize.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
test('should initialize LiteMCP in both modes', async () => {
|
||||
// Test OpenAI mode
|
||||
process.env.PROCESSOR_TYPE = 'openai';
|
||||
await import('../src/index.js');
|
||||
test('should handle server shutdown gracefully', async () => {
|
||||
// Enable speech service for shutdown test
|
||||
process.env.SPEECH_ENABLED = 'true';
|
||||
|
||||
expect(mockLiteMCP.mock.calls.length).toBeGreaterThan(0);
|
||||
const [name, version] = mockLiteMCP.mock.calls[0] ?? [];
|
||||
expect(name).toBe('home-assistant');
|
||||
expect(typeof version).toBe('string');
|
||||
// Import and initialize server
|
||||
const mod = await import('../src/index');
|
||||
|
||||
// Reset for next test
|
||||
mockLiteMCP.mockReset();
|
||||
// Simulate SIGTERM
|
||||
process.emit('SIGTERM');
|
||||
|
||||
// Test Claude mode
|
||||
process.env.PROCESSOR_TYPE = 'claude';
|
||||
await import('../src/index.js');
|
||||
|
||||
expect(mockLiteMCP.mock.calls.length).toBeGreaterThan(0);
|
||||
const [name2, version2] = mockLiteMCP.mock.calls[0] ?? [];
|
||||
expect(name2).toBe('home-assistant');
|
||||
expect(typeof version2).toBe('string');
|
||||
});
|
||||
|
||||
test('should handle missing PROCESSOR_TYPE (default to Express server)', async () => {
|
||||
// Remove PROCESSOR_TYPE
|
||||
delete process.env.PROCESSOR_TYPE;
|
||||
|
||||
// Import the main module
|
||||
await import('../src/index.js');
|
||||
|
||||
// Verify Express server was initialized (default behavior)
|
||||
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);
|
||||
// Verify shutdown behavior
|
||||
expect(mockSpeechService.shutdown.mock.calls.length).toBe(1);
|
||||
expect(consoleLog.mock.calls.some(call =>
|
||||
typeof call.args[0] === 'string' &&
|
||||
call.args[0].includes('Shutting down gracefully')
|
||||
)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,81 +1,79 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
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';
|
||||
import { describe, expect, test, beforeEach, afterEach, mock, spyOn } from "bun:test";
|
||||
import type { Mock } from "bun:test";
|
||||
import { EventEmitter } from "events";
|
||||
import { SpeechToText, TranscriptionError, type TranscriptionOptions } from "../../src/speech/speechToText";
|
||||
import type { SpeechToTextConfig } from "../../src/speech/types";
|
||||
import type { ChildProcess } from "child_process";
|
||||
|
||||
// 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);
|
||||
interface MockProcess extends EventEmitter {
|
||||
stdout: EventEmitter;
|
||||
stderr: EventEmitter;
|
||||
kill: Mock<() => void>;
|
||||
}
|
||||
}));
|
||||
|
||||
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'
|
||||
type SpawnFn = {
|
||||
(cmds: string[], options?: Record<string, unknown>): ChildProcess;
|
||||
};
|
||||
|
||||
describe('SpeechToText', () => {
|
||||
let spawnMock: Mock<SpawnFn>;
|
||||
let mockProcess: MockProcess;
|
||||
let speechToText: SpeechToText;
|
||||
|
||||
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();
|
||||
// Create mock process
|
||||
mockProcess = new EventEmitter() as MockProcess;
|
||||
mockProcess.stdout = new EventEmitter();
|
||||
mockProcess.stderr = new EventEmitter();
|
||||
mockProcess.kill = mock(() => { });
|
||||
|
||||
// Create spawn mock
|
||||
spawnMock = mock((cmds: string[], options?: Record<string, unknown>) => mockProcess as unknown as ChildProcess);
|
||||
(globalThis as any).Bun = { spawn: spawnMock };
|
||||
|
||||
// Initialize SpeechToText
|
||||
const config: SpeechToTextConfig = {
|
||||
modelPath: '/test/model',
|
||||
modelType: 'base.en',
|
||||
containerName: 'test-container'
|
||||
};
|
||||
speechToText = new SpeechToText(config);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
speechToText.stopWakeWordDetection();
|
||||
// Clean up test files
|
||||
if (fs.existsSync(testAudioDir)) {
|
||||
fs.rmSync(testAudioDir, { recursive: true, force: true });
|
||||
}
|
||||
// Cleanup
|
||||
mockProcess.removeAllListeners();
|
||||
mockProcess.stdout.removeAllListeners();
|
||||
mockProcess.stderr.removeAllListeners();
|
||||
});
|
||||
|
||||
describe('Initialization', () => {
|
||||
test('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);
|
||||
const config: SpeechToTextConfig = {
|
||||
modelPath: '/test/model',
|
||||
modelType: 'base.en'
|
||||
};
|
||||
const instance = new SpeechToText(config);
|
||||
expect(instance).toBeDefined();
|
||||
});
|
||||
|
||||
test('should initialize successfully', async () => {
|
||||
const initSpy = spyOn(speechToText, 'initialize');
|
||||
await speechToText.initialize();
|
||||
expect(initSpy).toHaveBeenCalled();
|
||||
const result = await speechToText.initialize();
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should not initialize twice', async () => {
|
||||
await speechToText.initialize();
|
||||
const initSpy = spyOn(speechToText, 'initialize');
|
||||
await speechToText.initialize();
|
||||
expect(initSpy.mock.calls.length).toBe(1);
|
||||
const result = await speechToText.initialize();
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Health Check', () => {
|
||||
test('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);
|
||||
|
||||
// Setup mock process
|
||||
setTimeout(() => {
|
||||
mockProcess.stdout.emtest('data', Buffer.from('Up 2 hours'));
|
||||
mockProcess.stdout.emit('data', Buffer.from('Up 2 hours'));
|
||||
}, 0);
|
||||
|
||||
const result = await speechToText.checkHealth();
|
||||
@@ -83,23 +81,20 @@ describe('SpeechToText', () => {
|
||||
});
|
||||
|
||||
test('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);
|
||||
// Setup mock process
|
||||
setTimeout(() => {
|
||||
mockProcess.stdout.emit('data', Buffer.from('No containers found'));
|
||||
}, 0);
|
||||
|
||||
const result = await speechToText.checkHealth();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle Docker command errors', async () => {
|
||||
spawnMock.mockImplementation(() => {
|
||||
throw new Error('Docker not found');
|
||||
});
|
||||
// Setup mock process
|
||||
setTimeout(() => {
|
||||
mockProcess.stderr.emit('data', Buffer.from('Docker error'));
|
||||
}, 0);
|
||||
|
||||
const result = await speechToText.checkHealth();
|
||||
expect(result).toBe(false);
|
||||
@@ -108,51 +103,48 @@ describe('SpeechToText', () => {
|
||||
|
||||
describe('Wake Word Detection', () => {
|
||||
test('should detect wake word and emit event', async () => {
|
||||
const testFile = path.join(testAudioDir, 'wake_word_test_123456.wav');
|
||||
const testMetadata = `${testFile}.json`;
|
||||
// Setup mock process
|
||||
setTimeout(() => {
|
||||
mockProcess.stdout.emit('data', Buffer.from('Wake word detected'));
|
||||
}, 0);
|
||||
|
||||
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');
|
||||
const wakeWordPromise = new Promise<void>((resolve) => {
|
||||
speechToText.on('wake_word', () => {
|
||||
resolve();
|
||||
});
|
||||
|
||||
// Create a test audio file to trigger the event
|
||||
fs.writeFileSync(testFile, 'test audio content');
|
||||
});
|
||||
|
||||
speechToText.startWakeWordDetection();
|
||||
await wakeWordPromise;
|
||||
});
|
||||
|
||||
test('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');
|
||||
|
||||
// Setup mock process
|
||||
setTimeout(() => {
|
||||
expect(eventEmitted).toBe(false);
|
||||
mockProcess.stdout.emit('data', Buffer.from('Processing audio'));
|
||||
}, 0);
|
||||
|
||||
const wakeWordPromise = new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
resolve();
|
||||
}, 100);
|
||||
|
||||
speechToText.on('wake_word', () => {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error('Wake word should not be detected'));
|
||||
});
|
||||
});
|
||||
|
||||
speechToText.startWakeWordDetection();
|
||||
await wakeWordPromise;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Audio Transcription', () => {
|
||||
const mockTranscriptionResult: TranscriptionResult = {
|
||||
text: 'Hello world',
|
||||
const mockTranscriptionResult = {
|
||||
text: 'Test transcription',
|
||||
segments: [{
|
||||
text: 'Hello world',
|
||||
text: 'Test transcription',
|
||||
start: 0,
|
||||
end: 1,
|
||||
confidence: 0.95
|
||||
@@ -160,169 +152,100 @@ describe('SpeechToText', () => {
|
||||
};
|
||||
|
||||
test('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');
|
||||
|
||||
// Setup mock process
|
||||
setTimeout(() => {
|
||||
mockProcess.stdout.emtest('data', Buffer.from(JSON.stringify(mockTranscriptionResult)));
|
||||
mockProcess.stdout.emit('data', Buffer.from(JSON.stringify(mockTranscriptionResult)));
|
||||
}, 0);
|
||||
|
||||
const result = await transcriptionPromise;
|
||||
const result = await speechToText.transcribeAudio('/test/audio.wav');
|
||||
expect(result).toEqual(mockTranscriptionResult);
|
||||
});
|
||||
|
||||
test('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');
|
||||
|
||||
// Setup mock process
|
||||
setTimeout(() => {
|
||||
mockProcess.stderr.emtest('data', Buffer.from('Transcription failed'));
|
||||
mockProcess.stderr.emit('data', Buffer.from('Transcription failed'));
|
||||
}, 0);
|
||||
|
||||
await expect(transcriptionPromise).rejects.toThrow(TranscriptionError);
|
||||
await expect(speechToText.transcribeAudio('/test/audio.wav')).rejects.toThrow(TranscriptionError);
|
||||
});
|
||||
|
||||
test('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');
|
||||
|
||||
// Setup mock process
|
||||
setTimeout(() => {
|
||||
mockProcess.stdout.emtest('data', Buffer.from('Invalid JSON'));
|
||||
mockProcess.stdout.emit('data', Buffer.from('Invalid JSON'));
|
||||
}, 0);
|
||||
|
||||
await expect(transcriptionPromise).rejects.toThrow(TranscriptionError);
|
||||
await expect(speechToText.transcribeAudio('/test/audio.wav')).rejects.toThrow(TranscriptionError);
|
||||
});
|
||||
|
||||
test('should pass correct transcription options', async () => {
|
||||
const options: TranscriptionOptions = {
|
||||
model: 'large-v2',
|
||||
model: 'base.en',
|
||||
language: 'en',
|
||||
temperature: 0.5,
|
||||
beamSize: 3,
|
||||
patience: 2,
|
||||
device: 'cuda'
|
||||
temperature: 0,
|
||||
beamSize: 5,
|
||||
patience: 1,
|
||||
device: 'cpu'
|
||||
};
|
||||
|
||||
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);
|
||||
await speechToText.transcribeAudio('/test/audio.wav', options);
|
||||
|
||||
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(() => { });
|
||||
const spawnArgs = spawnMock.mock.calls[0]?.args[1] || [];
|
||||
expect(spawnArgs).toContain('--model');
|
||||
expect(spawnArgs).toContain(options.model);
|
||||
expect(spawnArgs).toContain('--language');
|
||||
expect(spawnArgs).toContain(options.language);
|
||||
expect(spawnArgs).toContain('--temperature');
|
||||
expect(spawnArgs).toContain(options.temperature?.toString());
|
||||
expect(spawnArgs).toContain('--beam-size');
|
||||
expect(spawnArgs).toContain(options.beamSize?.toString());
|
||||
expect(spawnArgs).toContain('--patience');
|
||||
expect(spawnArgs).toContain(options.patience?.toString());
|
||||
expect(spawnArgs).toContain('--device');
|
||||
expect(spawnArgs).toContain(options.device);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Event Handling', () => {
|
||||
test('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' }
|
||||
]);
|
||||
const progressPromise = new Promise<void>((resolve) => {
|
||||
speechToText.on('progress', (progress) => {
|
||||
expect(progress).toEqual({ type: 'stdout', data: 'Processing' });
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
void speechToText.transcribeAudio('/test/audio.wav');
|
||||
|
||||
mockProcess.stdout.emtest('data', Buffer.from('Processing'));
|
||||
mockProcess.stderr.emtest('data', Buffer.from('Loading model'));
|
||||
});
|
||||
const transcribePromise = speechToText.transcribeAudio('/test/audio.wav');
|
||||
mockProcess.stdout.emit('data', Buffer.from('Processing'));
|
||||
await Promise.all([transcribePromise.catch(() => { }), progressPromise]);
|
||||
});
|
||||
|
||||
test('should emit error events', async () => {
|
||||
return new Promise<void>((resolve) => {
|
||||
const errorPromise = new Promise<void>((resolve) => {
|
||||
speechToText.on('error', (error) => {
|
||||
expect(error instanceof Error).toBe(true);
|
||||
expect(error.message).toBe('Test error');
|
||||
resolve();
|
||||
});
|
||||
|
||||
speechToText.emtest('error', new Error('Test error'));
|
||||
});
|
||||
|
||||
speechToText.emit('error', new Error('Test error'));
|
||||
await errorPromise;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cleanup', () => {
|
||||
test('should stop wake word detection', () => {
|
||||
speechToText.startWakeWordDetection(testAudioDir);
|
||||
speechToText.startWakeWordDetection();
|
||||
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);
|
||||
expect(mockProcess.kill.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
test('should clean up resources on shutdown', async () => {
|
||||
await speechToText.initialize();
|
||||
const shutdownSpy = spyOn(speechToText, 'shutdown');
|
||||
await speechToText.shutdown();
|
||||
expect(shutdownSpy).toHaveBeenCalled();
|
||||
expect(mockProcess.kill.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,120 +1,182 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
||||
import { HassWebSocketClient } from '../../src/websocket/client.js';
|
||||
import WebSocket from 'ws';
|
||||
import { EventEmitter } from 'events';
|
||||
import * as HomeAssistant from '../../src/types/hass.js';
|
||||
|
||||
// Mock WebSocket
|
||||
// // jest.mock('ws');
|
||||
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
|
||||
import { EventEmitter } from "events";
|
||||
import { HassWebSocketClient } from "../../src/websocket/client";
|
||||
import type { MessageEvent, ErrorEvent } from "ws";
|
||||
import { Mock, fn as jestMock } from 'jest-mock';
|
||||
import { expect as jestExpect } from '@jest/globals';
|
||||
|
||||
describe('WebSocket Event Handling', () => {
|
||||
let client: HassWebSocketClient;
|
||||
let mockWebSocket: jest.Mocked<WebSocket>;
|
||||
let mockWebSocket: any;
|
||||
let onOpenCallback: () => void;
|
||||
let onCloseCallback: () => void;
|
||||
let onErrorCallback: (event: any) => void;
|
||||
let onMessageCallback: (event: any) => void;
|
||||
let eventEmitter: EventEmitter;
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear all mocks
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Create event emitter for mocking WebSocket events
|
||||
eventEmitter = new EventEmitter();
|
||||
|
||||
// Create mock WebSocket instance
|
||||
// Initialize callbacks first
|
||||
onOpenCallback = () => { };
|
||||
onCloseCallback = () => { };
|
||||
onErrorCallback = () => { };
|
||||
onMessageCallback = () => { };
|
||||
|
||||
mockWebSocket = {
|
||||
on: jest.fn((event: string, listener: (...args: any[]) => void) => {
|
||||
eventEmitter.on(event, listener);
|
||||
return mockWebSocket;
|
||||
}),
|
||||
send: mock(),
|
||||
close: mock(),
|
||||
readyState: WebSocket.OPEN,
|
||||
removeAllListeners: mock(),
|
||||
// Add required WebSocket properties
|
||||
binaryType: 'arraybuffer',
|
||||
bufferedAmount: 0,
|
||||
extensions: '',
|
||||
protocol: '',
|
||||
url: 'ws://test.com',
|
||||
isPaused: () => false,
|
||||
ping: mock(),
|
||||
pong: mock(),
|
||||
terminate: mock()
|
||||
} as unknown as jest.Mocked<WebSocket>;
|
||||
readyState: 1,
|
||||
OPEN: 1,
|
||||
onopen: null,
|
||||
onclose: null,
|
||||
onerror: null,
|
||||
onmessage: null
|
||||
};
|
||||
|
||||
// Mock WebSocket constructor
|
||||
(WebSocket as unknown as jest.Mock).mockImplementation(() => mockWebSocket);
|
||||
// Define setters that store the callbacks
|
||||
Object.defineProperties(mockWebSocket, {
|
||||
onopen: {
|
||||
get() { return onOpenCallback; },
|
||||
set(callback: () => void) { onOpenCallback = callback; }
|
||||
},
|
||||
onclose: {
|
||||
get() { return onCloseCallback; },
|
||||
set(callback: () => void) { onCloseCallback = callback; }
|
||||
},
|
||||
onerror: {
|
||||
get() { return onErrorCallback; },
|
||||
set(callback: (event: any) => void) { onErrorCallback = callback; }
|
||||
},
|
||||
onmessage: {
|
||||
get() { return onMessageCallback; },
|
||||
set(callback: (event: any) => void) { onMessageCallback = callback; }
|
||||
}
|
||||
});
|
||||
|
||||
// Create client instance
|
||||
client = new HassWebSocketClient('ws://test.com', 'test-token');
|
||||
// @ts-expect-error - Mock WebSocket implementation
|
||||
global.WebSocket = mock(() => mockWebSocket);
|
||||
|
||||
client = new HassWebSocketClient('ws://localhost:8123/api/websocket', 'test-token');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (eventEmitter) {
|
||||
eventEmitter.removeAllListeners();
|
||||
}
|
||||
if (client) {
|
||||
client.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle connection events', () => {
|
||||
// Simulate open event
|
||||
eventEmitter.emtest('open');
|
||||
|
||||
// Verify authentication message was sent
|
||||
expect(mockWebSocket.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('"type":"auth"')
|
||||
);
|
||||
test('should handle connection events', async () => {
|
||||
const connectPromise = client.connect();
|
||||
onOpenCallback();
|
||||
await connectPromise;
|
||||
expect(client.isConnected()).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle authentication response', () => {
|
||||
// Simulate auth_ok message
|
||||
eventEmitter.emtest('message', JSON.stringify({ type: 'auth_ok' }));
|
||||
test('should handle authentication response', async () => {
|
||||
const connectPromise = client.connect();
|
||||
onOpenCallback();
|
||||
|
||||
// Verify client is ready for commands
|
||||
expect(mockWebSocket.readyState).toBe(WebSocket.OPEN);
|
||||
onMessageCallback({
|
||||
data: JSON.stringify({
|
||||
type: 'auth_required'
|
||||
})
|
||||
});
|
||||
|
||||
test('should handle auth failure', () => {
|
||||
// Simulate auth_invalid message
|
||||
eventEmitter.emtest('message', JSON.stringify({
|
||||
onMessageCallback({
|
||||
data: JSON.stringify({
|
||||
type: 'auth_ok'
|
||||
})
|
||||
});
|
||||
|
||||
await connectPromise;
|
||||
expect(client.isAuthenticated()).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle auth failure', async () => {
|
||||
const connectPromise = client.connect();
|
||||
onOpenCallback();
|
||||
|
||||
onMessageCallback({
|
||||
data: JSON.stringify({
|
||||
type: 'auth_required'
|
||||
})
|
||||
});
|
||||
|
||||
onMessageCallback({
|
||||
data: JSON.stringify({
|
||||
type: 'auth_invalid',
|
||||
message: 'Invalid token'
|
||||
}));
|
||||
|
||||
// Verify client attempts to close connection
|
||||
expect(mockWebSocket.close).toHaveBeenCalled();
|
||||
message: 'Invalid password'
|
||||
})
|
||||
});
|
||||
|
||||
test('should handle connection errors', () => {
|
||||
// Create error spy
|
||||
const errorSpy = mock();
|
||||
client.on('error', errorSpy);
|
||||
|
||||
// Simulate error
|
||||
const testError = new Error('Test error');
|
||||
eventEmitter.emtest('error', testError);
|
||||
|
||||
// Verify error was handled
|
||||
expect(errorSpy).toHaveBeenCalledWith(testError);
|
||||
await expect(connectPromise).rejects.toThrow('Authentication failed');
|
||||
expect(client.isAuthenticated()).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle disconnection', () => {
|
||||
// Create close spy
|
||||
const closeSpy = mock();
|
||||
client.on('close', closeSpy);
|
||||
|
||||
// Simulate close
|
||||
eventEmitter.emtest('close');
|
||||
|
||||
// Verify close was handled
|
||||
expect(closeSpy).toHaveBeenCalled();
|
||||
test('should handle connection errors', async () => {
|
||||
const errorPromise = new Promise((resolve) => {
|
||||
client.on('error', resolve);
|
||||
});
|
||||
|
||||
test('should handle event messages', () => {
|
||||
// Create event spy
|
||||
const eventSpy = mock();
|
||||
client.on('event', eventSpy);
|
||||
const connectPromise = client.connect().catch(() => { });
|
||||
onOpenCallback();
|
||||
|
||||
const errorEvent = {
|
||||
error: new Error('Connection failed'),
|
||||
message: 'Connection failed',
|
||||
target: mockWebSocket
|
||||
};
|
||||
|
||||
onErrorCallback(errorEvent);
|
||||
|
||||
const error = await errorPromise;
|
||||
expect(error).toBeDefined();
|
||||
expect((error as Error).message).toBe('Connection failed');
|
||||
});
|
||||
|
||||
test('should handle disconnection', async () => {
|
||||
const connectPromise = client.connect();
|
||||
onOpenCallback();
|
||||
await connectPromise;
|
||||
|
||||
const disconnectPromise = new Promise((resolve) => {
|
||||
client.on('disconnected', resolve);
|
||||
});
|
||||
|
||||
onCloseCallback();
|
||||
|
||||
await disconnectPromise;
|
||||
expect(client.isConnected()).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle event messages', async () => {
|
||||
const connectPromise = client.connect();
|
||||
onOpenCallback();
|
||||
|
||||
onMessageCallback({
|
||||
data: JSON.stringify({
|
||||
type: 'auth_required'
|
||||
})
|
||||
});
|
||||
|
||||
onMessageCallback({
|
||||
data: JSON.stringify({
|
||||
type: 'auth_ok'
|
||||
})
|
||||
});
|
||||
|
||||
await connectPromise;
|
||||
|
||||
const eventPromise = new Promise((resolve) => {
|
||||
client.on('state_changed', resolve);
|
||||
});
|
||||
|
||||
// Simulate event message
|
||||
const eventData = {
|
||||
id: 1,
|
||||
type: 'event',
|
||||
event: {
|
||||
event_type: 'state_changed',
|
||||
@@ -124,217 +186,63 @@ describe('WebSocket Event Handling', () => {
|
||||
}
|
||||
}
|
||||
};
|
||||
eventEmitter.emtest('message', JSON.stringify(eventData));
|
||||
|
||||
// Verify event was handled
|
||||
expect(eventSpy).toHaveBeenCalledWith(eventData.event);
|
||||
onMessageCallback({
|
||||
data: JSON.stringify(eventData)
|
||||
});
|
||||
|
||||
describe('Connection Events', () => {
|
||||
test('should handle successful connection', (done) => {
|
||||
client.on('open', () => {
|
||||
expect(mockWebSocket.send).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
eventEmitter.emtest('open');
|
||||
});
|
||||
|
||||
test('should handle connection errors', (done) => {
|
||||
const error = new Error('Connection failed');
|
||||
client.on('error', (err: Error) => {
|
||||
expect(err).toBe(error);
|
||||
done();
|
||||
});
|
||||
|
||||
eventEmitter.emtest('error', error);
|
||||
});
|
||||
|
||||
test('should handle connection close', (done) => {
|
||||
client.on('disconnected', () => {
|
||||
expect(mockWebSocket.close).toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
eventEmitter.emtest('close');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Authentication', () => {
|
||||
test('should send authentication message on connect', () => {
|
||||
const authMessage: HomeAssistant.AuthMessage = {
|
||||
type: 'auth',
|
||||
access_token: 'test_token'
|
||||
};
|
||||
|
||||
client.connect();
|
||||
expect(mockWebSocket.send).toHaveBeenCalledWith(JSON.stringify(authMessage));
|
||||
});
|
||||
|
||||
test('should handle successful authentication', (done) => {
|
||||
client.on('auth_ok', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
client.connect();
|
||||
eventEmitter.emtest('message', JSON.stringify({ type: 'auth_ok' }));
|
||||
});
|
||||
|
||||
test('should handle authentication failure', (done) => {
|
||||
client.on('auth_invalid', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
client.connect();
|
||||
eventEmitter.emtest('message', JSON.stringify({ type: 'auth_invalid' }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Event Subscription', () => {
|
||||
test('should handle state changed events', (done) => {
|
||||
const stateEvent: HomeAssistant.StateChangedEvent = {
|
||||
event_type: 'state_changed',
|
||||
data: {
|
||||
entity_id: 'light.living_room',
|
||||
new_state: {
|
||||
entity_id: 'light.living_room',
|
||||
state: 'on',
|
||||
attributes: { brightness: 255 },
|
||||
last_changed: '2024-01-01T00:00:00Z',
|
||||
last_updated: '2024-01-01T00:00:00Z',
|
||||
context: {
|
||||
id: '123',
|
||||
parent_id: null,
|
||||
user_id: null
|
||||
}
|
||||
},
|
||||
old_state: {
|
||||
entity_id: 'light.living_room',
|
||||
state: 'off',
|
||||
attributes: {},
|
||||
last_changed: '2024-01-01T00:00:00Z',
|
||||
last_updated: '2024-01-01T00:00:00Z',
|
||||
context: {
|
||||
id: '122',
|
||||
parent_id: null,
|
||||
user_id: null
|
||||
}
|
||||
}
|
||||
},
|
||||
origin: 'LOCAL',
|
||||
time_fired: '2024-01-01T00:00:00Z',
|
||||
context: {
|
||||
id: '123',
|
||||
parent_id: null,
|
||||
user_id: null
|
||||
}
|
||||
};
|
||||
|
||||
client.on('event', (event) => {
|
||||
expect(event.data.entity_id).toBe('light.living_room');
|
||||
expect(event.data.new_state.state).toBe('on');
|
||||
expect(event.data.old_state.state).toBe('off');
|
||||
done();
|
||||
});
|
||||
|
||||
eventEmitter.emtest('message', JSON.stringify({ type: 'event', event: stateEvent }));
|
||||
const receivedEvent = await eventPromise;
|
||||
expect(receivedEvent).toEqual(eventData.event.data);
|
||||
});
|
||||
|
||||
test('should subscribe to specific events', async () => {
|
||||
const subscriptionId = 1;
|
||||
const callback = mock();
|
||||
const connectPromise = client.connect();
|
||||
onOpenCallback();
|
||||
|
||||
// Mock successful subscription
|
||||
const subscribePromise = client.subscribeEvents('state_changed', callback);
|
||||
eventEmitter.emtest('message', JSON.stringify({
|
||||
id: 1,
|
||||
type: 'result',
|
||||
success: true
|
||||
}));
|
||||
onMessageCallback({
|
||||
data: JSON.stringify({
|
||||
type: 'auth_required'
|
||||
})
|
||||
});
|
||||
|
||||
await expect(subscribePromise).resolves.toBe(subscriptionId);
|
||||
onMessageCallback({
|
||||
data: JSON.stringify({
|
||||
type: 'auth_ok'
|
||||
})
|
||||
});
|
||||
|
||||
// Test event handling
|
||||
const eventData = {
|
||||
entity_id: 'light.living_room',
|
||||
state: 'on'
|
||||
};
|
||||
eventEmitter.emtest('message', JSON.stringify({
|
||||
type: 'event',
|
||||
event: {
|
||||
event_type: 'state_changed',
|
||||
data: eventData
|
||||
}
|
||||
}));
|
||||
await connectPromise;
|
||||
|
||||
expect(callback).toHaveBeenCalledWith(eventData);
|
||||
const subscriptionId = await client.subscribeEvents('state_changed', (data) => {
|
||||
// Empty callback for type satisfaction
|
||||
});
|
||||
expect(mockWebSocket.send).toHaveBeenCalled();
|
||||
expect(subscriptionId).toBeDefined();
|
||||
});
|
||||
|
||||
test('should unsubscribe from events', async () => {
|
||||
// First subscribe
|
||||
const subscriptionId = await client.subscribeEvents('state_changed', () => { });
|
||||
const connectPromise = client.connect();
|
||||
onOpenCallback();
|
||||
|
||||
// Then unsubscribe
|
||||
const unsubscribePromise = client.unsubscribeEvents(subscriptionId);
|
||||
eventEmitter.emtest('message', JSON.stringify({
|
||||
id: 2,
|
||||
type: 'result',
|
||||
success: true
|
||||
}));
|
||||
|
||||
await expect(unsubscribePromise).resolves.toBeUndefined();
|
||||
});
|
||||
onMessageCallback({
|
||||
data: JSON.stringify({
|
||||
type: 'auth_required'
|
||||
})
|
||||
});
|
||||
|
||||
describe('Message Handling', () => {
|
||||
test('should handle malformed messages', (done) => {
|
||||
client.on('error', (error: Error) => {
|
||||
expect(error.message).toContain('Unexpected token');
|
||||
done();
|
||||
onMessageCallback({
|
||||
data: JSON.stringify({
|
||||
type: 'auth_ok'
|
||||
})
|
||||
});
|
||||
|
||||
eventEmitter.emtest('message', 'invalid json');
|
||||
});
|
||||
await connectPromise;
|
||||
|
||||
test('should handle unknown message types', (done) => {
|
||||
const unknownMessage = {
|
||||
type: 'unknown_type',
|
||||
data: {}
|
||||
};
|
||||
const subscriptionId = await client.subscribeEvents('state_changed', (data) => {
|
||||
// Empty callback for type satisfaction
|
||||
});
|
||||
await client.unsubscribeEvents(subscriptionId);
|
||||
|
||||
client.on('error', (error: Error) => {
|
||||
expect(error.message).toContain('Unknown message type');
|
||||
done();
|
||||
});
|
||||
|
||||
eventEmitter.emtest('message', JSON.stringify(unknownMessage));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reconnection', () => {
|
||||
test('should attempt to reconnect on connection loss', (done) => {
|
||||
let reconnectAttempts = 0;
|
||||
client.on('disconnected', () => {
|
||||
reconnectAttempts++;
|
||||
if (reconnectAttempts === 1) {
|
||||
expect(WebSocket).toHaveBeenCalledTimes(2);
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
eventEmitter.emtest('close');
|
||||
});
|
||||
|
||||
test('should re-authenticate after reconnection', (done) => {
|
||||
client.connect();
|
||||
|
||||
client.on('auth_ok', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
eventEmitter.emtest('close');
|
||||
eventEmitter.emtest('open');
|
||||
eventEmitter.emtest('message', JSON.stringify({ type: 'auth_ok' }));
|
||||
});
|
||||
expect(mockWebSocket.send).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -232,3 +232,11 @@ The current API version is v1. Include the version in the URL:
|
||||
- [Core Functions](core.md) - Detailed endpoint documentation
|
||||
- [Architecture Overview](../architecture.md) - System design details
|
||||
- [Troubleshooting](../troubleshooting.md) - Common issues and solutions
|
||||
|
||||
# API Reference
|
||||
|
||||
The Advanced Home Assistant MCP provides several APIs for integration and automation:
|
||||
|
||||
- [Core API](core.md) - Primary interface for system control
|
||||
- [SSE API](sse.md) - Server-Sent Events for real-time updates
|
||||
- [Core Functions](core.md) - Essential system functions
|
||||
30
docs/config/index.md
Normal file
30
docs/config/index.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Configuration
|
||||
|
||||
This section covers the configuration options available in the Home Assistant MCP Server.
|
||||
|
||||
## Overview
|
||||
|
||||
The MCP Server can be configured through various configuration files and environment variables. This section will guide you through the available options and their usage.
|
||||
|
||||
## Configuration Files
|
||||
|
||||
The main configuration files are:
|
||||
|
||||
1. `.env` - Environment variables
|
||||
2. `config.yaml` - Main configuration file
|
||||
3. `devices.yaml` - Device-specific configurations
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Key environment variables that can be set:
|
||||
|
||||
- `MCP_HOST` - Host address (default: 0.0.0.0)
|
||||
- `MCP_PORT` - Port number (default: 8123)
|
||||
- `MCP_LOG_LEVEL` - Logging level (default: INFO)
|
||||
- `MCP_CONFIG_DIR` - Configuration directory path
|
||||
|
||||
## Next Steps
|
||||
|
||||
- See [System Configuration](../configuration.md) for detailed configuration options
|
||||
- Check [Environment Setup](../getting-started/configuration.md) for initial setup
|
||||
- Review [Security](../security.md) for security-related configurations
|
||||
270
docs/configuration.md
Normal file
270
docs/configuration.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# System Configuration
|
||||
|
||||
This document provides detailed information about configuring the Home Assistant MCP Server.
|
||||
|
||||
## Configuration File Structure
|
||||
|
||||
The MCP Server uses environment variables for configuration, with support for different environments (development, test, production):
|
||||
|
||||
```bash
|
||||
# .env, .env.development, or .env.test
|
||||
PORT=4000
|
||||
NODE_ENV=development
|
||||
HASS_HOST=http://192.168.178.63:8123
|
||||
HASS_TOKEN=your_token_here
|
||||
JWT_SECRET=your_secret_key
|
||||
```
|
||||
|
||||
## Server Settings
|
||||
|
||||
### Basic Server Configuration
|
||||
- `PORT`: Server port number (default: 4000)
|
||||
- `NODE_ENV`: Environment mode (development, production, test)
|
||||
- `HASS_HOST`: Home Assistant instance URL
|
||||
- `HASS_TOKEN`: Home Assistant long-lived access token
|
||||
|
||||
### Security Settings
|
||||
- `JWT_SECRET`: Secret key for JWT token generation
|
||||
- `RATE_LIMIT`: Rate limiting configuration
|
||||
- `windowMs`: Time window in milliseconds (default: 15 minutes)
|
||||
- `max`: Maximum requests per window (default: 100)
|
||||
|
||||
### WebSocket Settings
|
||||
- `SSE`: Server-Sent Events configuration
|
||||
- `MAX_CLIENTS`: Maximum concurrent clients (default: 1000)
|
||||
- `PING_INTERVAL`: Keep-alive ping interval in ms (default: 30000)
|
||||
|
||||
### Speech Features (Optional)
|
||||
- `ENABLE_SPEECH_FEATURES`: Enable speech processing features (default: false)
|
||||
- `ENABLE_WAKE_WORD`: Enable wake word detection (default: false)
|
||||
- `ENABLE_SPEECH_TO_TEXT`: Enable speech-to-text conversion (default: false)
|
||||
- `WHISPER_MODEL_PATH`: Path to Whisper models directory (default: /models)
|
||||
- `WHISPER_MODEL_TYPE`: Whisper model type (default: base)
|
||||
- Available models: tiny.en, base.en, small.en, medium.en, large-v2
|
||||
|
||||
## Environment Variables
|
||||
|
||||
All configuration is managed through environment variables:
|
||||
|
||||
```bash
|
||||
# Server
|
||||
PORT=4000
|
||||
NODE_ENV=development
|
||||
|
||||
# Home Assistant
|
||||
HASS_HOST=http://your-hass-instance:8123
|
||||
HASS_TOKEN=your_token_here
|
||||
|
||||
# Security
|
||||
JWT_SECRET=your-secret-key
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
LOG_DIR=logs
|
||||
LOG_MAX_SIZE=20m
|
||||
LOG_MAX_DAYS=14d
|
||||
LOG_COMPRESS=true
|
||||
LOG_REQUESTS=true
|
||||
|
||||
# Speech Features (Optional)
|
||||
ENABLE_SPEECH_FEATURES=false
|
||||
ENABLE_WAKE_WORD=false
|
||||
ENABLE_SPEECH_TO_TEXT=false
|
||||
WHISPER_MODEL_PATH=/models
|
||||
WHISPER_MODEL_TYPE=base
|
||||
```
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Security Rate Limiting
|
||||
Rate limiting is enabled by default to protect against brute force attacks:
|
||||
|
||||
```typescript
|
||||
RATE_LIMIT: {
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100 // limit each IP to 100 requests per window
|
||||
}
|
||||
```
|
||||
|
||||
### Logging
|
||||
The server uses Bun's built-in logging capabilities with additional configuration:
|
||||
|
||||
```typescript
|
||||
LOGGING: {
|
||||
LEVEL: "info", // debug, info, warn, error
|
||||
DIR: "logs",
|
||||
MAX_SIZE: "20m",
|
||||
MAX_DAYS: "14d",
|
||||
COMPRESS: true,
|
||||
TIMESTAMP_FORMAT: "YYYY-MM-DD HH:mm:ss:ms",
|
||||
LOG_REQUESTS: true
|
||||
}
|
||||
```
|
||||
|
||||
### Speech-to-Text Configuration
|
||||
When speech features are enabled, you can configure the following options:
|
||||
|
||||
```typescript
|
||||
SPEECH: {
|
||||
ENABLED: false, // Master switch for all speech features
|
||||
WAKE_WORD_ENABLED: false, // Enable wake word detection
|
||||
SPEECH_TO_TEXT_ENABLED: false, // Enable speech-to-text
|
||||
WHISPER_MODEL_PATH: "/models", // Path to Whisper models
|
||||
WHISPER_MODEL_TYPE: "base", // Model type to use
|
||||
}
|
||||
```
|
||||
|
||||
Available Whisper models:
|
||||
- `tiny.en`: Fastest, lowest accuracy
|
||||
- `base.en`: Good balance of speed and accuracy
|
||||
- `small.en`: Better accuracy, slower
|
||||
- `medium.en`: High accuracy, much slower
|
||||
- `large-v2`: Best accuracy, very slow
|
||||
|
||||
For production deployments, we recommend using system tools like `logrotate` for log management.
|
||||
|
||||
Example logrotate configuration (`/etc/logrotate.d/mcp-server`):
|
||||
```
|
||||
/var/log/mcp-server.log {
|
||||
daily
|
||||
rotate 7
|
||||
compress
|
||||
delaycompress
|
||||
missingok
|
||||
notifempty
|
||||
create 644 mcp mcp
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Always use environment variables for sensitive information
|
||||
2. Keep .env files secure and never commit them to version control
|
||||
3. Use different environment files for development, test, and production
|
||||
4. Enable SSL/TLS in production (preferably via reverse proxy)
|
||||
5. Monitor log files for issues
|
||||
6. Regularly rotate logs in production
|
||||
7. Start with smaller Whisper models and upgrade if needed
|
||||
8. Consider GPU acceleration for larger Whisper models
|
||||
|
||||
## Validation
|
||||
|
||||
The server validates configuration on startup using Zod schemas:
|
||||
- Required fields are checked (e.g., HASS_TOKEN)
|
||||
- Value types are verified
|
||||
- Enums are validated (e.g., LOG_LEVEL, WHISPER_MODEL_TYPE)
|
||||
- Default values are applied when not specified
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Common configuration issues:
|
||||
1. Missing required environment variables
|
||||
2. Invalid environment variable values
|
||||
3. Permission issues with log directories
|
||||
4. Rate limiting too restrictive
|
||||
5. Speech model loading failures
|
||||
6. Docker not available for speech features
|
||||
7. Insufficient system resources for larger models
|
||||
|
||||
See the [Troubleshooting Guide](troubleshooting.md) for solutions.
|
||||
|
||||
# Configuration Guide
|
||||
|
||||
This document describes all available configuration options for the Home Assistant MCP Server.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Required Settings
|
||||
|
||||
```bash
|
||||
# Server Configuration
|
||||
PORT=3000 # Server port
|
||||
HOST=localhost # Server host
|
||||
|
||||
# Home Assistant
|
||||
HASS_URL=http://localhost:8123 # Home Assistant URL
|
||||
HASS_TOKEN=your_token # Long-lived access token
|
||||
|
||||
# Security
|
||||
JWT_SECRET=your_secret # JWT signing secret
|
||||
```
|
||||
|
||||
### Optional Settings
|
||||
|
||||
```bash
|
||||
# Rate Limiting
|
||||
RATE_LIMIT_WINDOW=60000 # Time window in ms (default: 60000)
|
||||
RATE_LIMIT_MAX=100 # Max requests per window (default: 100)
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info # debug, info, warn, error (default: info)
|
||||
LOG_DIR=logs # Log directory (default: logs)
|
||||
LOG_MAX_SIZE=10m # Max log file size (default: 10m)
|
||||
LOG_MAX_FILES=5 # Max number of log files (default: 5)
|
||||
|
||||
# WebSocket/SSE
|
||||
WS_HEARTBEAT=30000 # WebSocket heartbeat interval in ms (default: 30000)
|
||||
SSE_RETRY=3000 # SSE retry interval in ms (default: 3000)
|
||||
|
||||
# Speech Features
|
||||
ENABLE_SPEECH_FEATURES=false # Enable speech processing (default: false)
|
||||
ENABLE_WAKE_WORD=false # Enable wake word detection (default: false)
|
||||
ENABLE_SPEECH_TO_TEXT=false # Enable speech-to-text (default: false)
|
||||
|
||||
# Speech Model Configuration
|
||||
WHISPER_MODEL_PATH=/models # Path to whisper models (default: /models)
|
||||
WHISPER_MODEL_TYPE=base # Model type: tiny|base|small|medium|large-v2 (default: base)
|
||||
WHISPER_LANGUAGE=en # Primary language (default: en)
|
||||
WHISPER_TASK=transcribe # Task type: transcribe|translate (default: transcribe)
|
||||
WHISPER_DEVICE=cuda # Processing device: cpu|cuda (default: cuda if available, else cpu)
|
||||
|
||||
# Wake Word Configuration
|
||||
WAKE_WORDS=hey jarvis,ok google,alexa # Comma-separated wake words (default: hey jarvis)
|
||||
WAKE_WORD_SENSITIVITY=0.5 # Detection sensitivity 0-1 (default: 0.5)
|
||||
```
|
||||
|
||||
## Speech Features
|
||||
|
||||
### Model Selection
|
||||
|
||||
Choose a model based on your needs:
|
||||
|
||||
| Model | Size | Memory Required | Speed | Accuracy |
|
||||
|------------|-------|-----------------|-------|----------|
|
||||
| tiny.en | 75MB | 1GB | Fast | Basic |
|
||||
| base.en | 150MB | 2GB | Good | Good |
|
||||
| small.en | 500MB | 4GB | Med | Better |
|
||||
| medium.en | 1.5GB | 8GB | Slow | High |
|
||||
| large-v2 | 3GB | 16GB | Slow | Best |
|
||||
|
||||
### GPU Acceleration
|
||||
|
||||
When `WHISPER_DEVICE=cuda`:
|
||||
- NVIDIA GPU with CUDA support required
|
||||
- Significantly faster processing
|
||||
- Higher memory requirements
|
||||
|
||||
### Wake Word Detection
|
||||
|
||||
- Multiple wake words supported via comma-separated list
|
||||
- Adjustable sensitivity (0-1):
|
||||
- Lower values: Fewer false positives, may miss some triggers
|
||||
- Higher values: More responsive, may have false triggers
|
||||
- Default (0.5): Balanced detection
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. Model Selection:
|
||||
- Start with `base.en` model
|
||||
- Upgrade if better accuracy needed
|
||||
- Downgrade if performance issues
|
||||
|
||||
2. Resource Management:
|
||||
- Monitor memory usage
|
||||
- Use GPU acceleration when available
|
||||
- Consider model size vs available resources
|
||||
|
||||
3. Wake Word Configuration:
|
||||
- Use distinct wake words
|
||||
- Adjust sensitivity based on environment
|
||||
- Limit number of wake words for better performance
|
||||
141
docs/deployment.md
Normal file
141
docs/deployment.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Deployment Guide
|
||||
|
||||
This documentation is automatically deployed to GitHub Pages using GitHub Actions. Here's how it works and how to manage deployments.
|
||||
|
||||
## Automatic Deployment
|
||||
|
||||
The documentation is automatically deployed when changes are pushed to the `main` or `master` branch. The deployment process:
|
||||
|
||||
1. Triggers on push to main/master
|
||||
2. Sets up Python environment
|
||||
3. Installs required dependencies
|
||||
4. Builds the documentation
|
||||
5. Deploys to the `gh-pages` branch
|
||||
|
||||
### GitHub Actions Workflow
|
||||
|
||||
The deployment is handled by the workflow in `.github/workflows/deploy-docs.yml`. This is the single source of truth for documentation deployment:
|
||||
|
||||
```yaml
|
||||
name: Deploy MkDocs
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
```
|
||||
|
||||
## Manual Deployment
|
||||
|
||||
If needed, you can deploy manually using:
|
||||
|
||||
```bash
|
||||
# Create a virtual environment
|
||||
python -m venv venv
|
||||
|
||||
# Activate the virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# Install dependencies
|
||||
pip install -r docs/requirements.txt
|
||||
|
||||
# Build the documentation
|
||||
mkdocs build
|
||||
|
||||
# Deploy to GitHub Pages
|
||||
mkdocs gh-deploy --force
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Documentation Updates
|
||||
- Test locally before pushing: `mkdocs serve`
|
||||
- Verify all links work
|
||||
- Ensure images are optimized
|
||||
- Check mobile responsiveness
|
||||
|
||||
### 2. Version Control
|
||||
- Keep documentation in sync with code versions
|
||||
- Use meaningful commit messages
|
||||
- Tag important documentation versions
|
||||
|
||||
### 3. Content Guidelines
|
||||
- Use consistent formatting
|
||||
- Keep navigation structure logical
|
||||
- Include examples where appropriate
|
||||
- Maintain up-to-date screenshots
|
||||
|
||||
### 4. Maintenance
|
||||
- Regularly review and update content
|
||||
- Check for broken links
|
||||
- Update dependencies
|
||||
- Monitor GitHub Actions logs
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Failed Deployments**
|
||||
- Check GitHub Actions logs
|
||||
- Verify dependencies are up to date
|
||||
- Ensure all required files exist
|
||||
|
||||
2. **Broken Links**
|
||||
- Run `mkdocs build --strict`
|
||||
- Use relative paths in markdown
|
||||
- Check case sensitivity
|
||||
|
||||
3. **Style Issues**
|
||||
- Verify theme configuration
|
||||
- Check CSS customizations
|
||||
- Test on multiple browsers
|
||||
|
||||
## Configuration Files
|
||||
|
||||
### requirements.txt
|
||||
|
||||
Create a requirements file for documentation dependencies:
|
||||
|
||||
```txt
|
||||
mkdocs-material
|
||||
mkdocs-minify-plugin
|
||||
mkdocs-git-revision-date-plugin
|
||||
mkdocs-mkdocstrings
|
||||
mkdocs-social-plugin
|
||||
mkdocs-redirects
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
- Check [GitHub Pages settings](https://github.com/jango-blockchained/advanced-homeassistant-mcp/settings/pages)
|
||||
- Monitor build status in Actions tab
|
||||
- Verify site accessibility
|
||||
|
||||
## Workflow Features
|
||||
|
||||
### Caching
|
||||
The workflow implements caching for Python dependencies to speed up deployments:
|
||||
- Pip cache for Python packages
|
||||
- MkDocs dependencies cache
|
||||
|
||||
### Deployment Checks
|
||||
Several checks are performed during deployment:
|
||||
1. Link validation with `mkdocs build --strict`
|
||||
2. Build verification
|
||||
3. Post-deployment site accessibility check
|
||||
|
||||
### Manual Triggers
|
||||
You can manually trigger deployments using the "workflow_dispatch" event in GitHub Actions.
|
||||
|
||||
## Cleanup
|
||||
|
||||
To clean up duplicate workflow files, run:
|
||||
|
||||
```bash
|
||||
# Make the script executable
|
||||
chmod +x scripts/cleanup-workflows.sh
|
||||
|
||||
# Run the cleanup script
|
||||
./scripts/cleanup-workflows.sh
|
||||
```
|
||||
@@ -1,190 +0,0 @@
|
||||
# Development Guide
|
||||
|
||||
This guide provides information for developers who want to contribute to or extend the Home Assistant MCP.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
homeassistant-mcp/
|
||||
├── src/
|
||||
│ ├── __tests__/ # Test files
|
||||
│ ├── __mocks__/ # Mock files
|
||||
│ ├── api/ # API endpoints and route handlers
|
||||
│ ├── config/ # Configuration management
|
||||
│ ├── hass/ # Home Assistant integration
|
||||
│ ├── interfaces/ # TypeScript interfaces
|
||||
│ ├── mcp/ # MCP core functionality
|
||||
│ ├── middleware/ # Express middleware
|
||||
│ ├── routes/ # Route definitions
|
||||
│ ├── security/ # Security utilities
|
||||
│ ├── sse/ # Server-Sent Events handling
|
||||
│ ├── tools/ # Tool implementations
|
||||
│ ├── types/ # TypeScript type definitions
|
||||
│ └── utils/ # Utility functions
|
||||
├── __tests__/ # Test files
|
||||
├── docs/ # Documentation
|
||||
├── dist/ # Compiled JavaScript
|
||||
└── scripts/ # Build and utility scripts
|
||||
```
|
||||
|
||||
## Development Setup
|
||||
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Set up development environment:
|
||||
```bash
|
||||
cp .env.example .env.development
|
||||
```
|
||||
|
||||
3. Start development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Code Style
|
||||
|
||||
We follow these coding standards:
|
||||
|
||||
1. TypeScript best practices
|
||||
- Use strict type checking
|
||||
- Avoid `any` types
|
||||
- Document complex types
|
||||
|
||||
2. ESLint rules
|
||||
- Run `npm run lint` to check
|
||||
- Run `npm run lint:fix` to auto-fix
|
||||
|
||||
3. Code formatting
|
||||
- Use Prettier
|
||||
- Run `npm run format` to format code
|
||||
|
||||
## Testing
|
||||
|
||||
1. Unit tests:
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
|
||||
2. Integration tests:
|
||||
```bash
|
||||
npm run test:integration
|
||||
```
|
||||
|
||||
3. Coverage report:
|
||||
```bash
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
## Creating New Tools
|
||||
|
||||
1. Create a new file in `src/tools/`:
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
import { Tool } from '../types';
|
||||
|
||||
export const myTool: Tool = {
|
||||
name: 'my_tool',
|
||||
description: 'Description of my tool',
|
||||
parameters: z.object({
|
||||
// Define parameters
|
||||
}),
|
||||
execute: async (params) => {
|
||||
// Implement tool logic
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
2. Add to `src/tools/index.ts`
|
||||
3. Create tests in `__tests__/tools/`
|
||||
4. Add documentation in `docs/tools/`
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Write/update tests
|
||||
5. Update documentation
|
||||
6. Submit a pull request
|
||||
|
||||
### Pull Request Process
|
||||
|
||||
1. Ensure all tests pass
|
||||
2. Update documentation
|
||||
3. Update CHANGELOG.md
|
||||
4. Get review from maintainers
|
||||
|
||||
## Building
|
||||
|
||||
1. Development build:
|
||||
```bash
|
||||
npm run build:dev
|
||||
```
|
||||
|
||||
2. Production build:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
1. Update documentation for changes
|
||||
2. Follow documentation structure
|
||||
3. Include examples
|
||||
4. Update type definitions
|
||||
|
||||
## Debugging
|
||||
|
||||
1. Development debugging:
|
||||
```bash
|
||||
npm run dev:debug
|
||||
```
|
||||
|
||||
2. Test debugging:
|
||||
```bash
|
||||
npm run test:debug
|
||||
```
|
||||
|
||||
3. VSCode launch configurations provided
|
||||
|
||||
## Performance
|
||||
|
||||
1. Follow performance best practices
|
||||
2. Use caching where appropriate
|
||||
3. Implement rate limiting
|
||||
4. Monitor memory usage
|
||||
|
||||
## Security
|
||||
|
||||
1. Follow security best practices
|
||||
2. Validate all inputs
|
||||
3. Use proper authentication
|
||||
4. Handle errors securely
|
||||
|
||||
## Deployment
|
||||
|
||||
1. Build for production:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. Start production server:
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
3. Docker deployment:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
Need development help?
|
||||
1. Check documentation
|
||||
2. Search issues
|
||||
3. Create new issue
|
||||
4. Join discussions
|
||||
197
docs/development/environment.md
Normal file
197
docs/development/environment.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Development Environment Setup
|
||||
|
||||
This guide will help you set up your development environment for the Home Assistant MCP Server.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Required Software
|
||||
- Python 3.10 or higher
|
||||
- pip (Python package manager)
|
||||
- git
|
||||
- Docker (optional, for containerized development)
|
||||
- Node.js 18+ (for frontend development)
|
||||
|
||||
### System Requirements
|
||||
- 4GB RAM minimum
|
||||
- 2 CPU cores minimum
|
||||
- 10GB free disk space
|
||||
|
||||
## Initial Setup
|
||||
|
||||
1. Clone the Repository
|
||||
```bash
|
||||
git clone https://github.com/jango-blockchained/homeassistant-mcp.git
|
||||
cd homeassistant-mcp
|
||||
```
|
||||
|
||||
2. Create Virtual Environment
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # Linux/macOS
|
||||
# or
|
||||
.venv\Scripts\activate # Windows
|
||||
```
|
||||
|
||||
3. Install Dependencies
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
pip install -r docs/requirements.txt # for documentation
|
||||
```
|
||||
|
||||
## Development Tools
|
||||
|
||||
### Code Editor Setup
|
||||
We recommend using Visual Studio Code with these extensions:
|
||||
- Python
|
||||
- Docker
|
||||
- YAML
|
||||
- ESLint
|
||||
- Prettier
|
||||
|
||||
### VS Code Settings
|
||||
```json
|
||||
{
|
||||
"python.linting.enabled": true,
|
||||
"python.linting.pylintEnabled": true,
|
||||
"python.formatting.provider": "black",
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Create Local Config
|
||||
```bash
|
||||
cp config.example.yaml config.yaml
|
||||
```
|
||||
|
||||
2. Set Environment Variables
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your settings
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Unit Tests
|
||||
```bash
|
||||
pytest tests/unit
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
```bash
|
||||
pytest tests/integration
|
||||
```
|
||||
|
||||
### Coverage Report
|
||||
```bash
|
||||
pytest --cov=src tests/
|
||||
```
|
||||
|
||||
## Docker Development
|
||||
|
||||
### Build Container
|
||||
```bash
|
||||
docker build -t mcp-server-dev -f Dockerfile.dev .
|
||||
```
|
||||
|
||||
### Run Development Container
|
||||
```bash
|
||||
docker run -it --rm \
|
||||
-v $(pwd):/app \
|
||||
-p 8123:8123 \
|
||||
mcp-server-dev
|
||||
```
|
||||
|
||||
## Database Setup
|
||||
|
||||
### Local Development Database
|
||||
```bash
|
||||
docker run -d \
|
||||
-p 5432:5432 \
|
||||
-e POSTGRES_USER=mcp \
|
||||
-e POSTGRES_PASSWORD=development \
|
||||
-e POSTGRES_DB=mcp_dev \
|
||||
postgres:14
|
||||
```
|
||||
|
||||
### Run Migrations
|
||||
```bash
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
## Frontend Development
|
||||
|
||||
1. Install Node.js Dependencies
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Start Development Server
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### Build Documentation
|
||||
```bash
|
||||
mkdocs serve
|
||||
```
|
||||
|
||||
### View Documentation
|
||||
Open http://localhost:8000 in your browser
|
||||
|
||||
## Debugging
|
||||
|
||||
### VS Code Launch Configuration
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: MCP Server",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "src/main.py",
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Git Hooks
|
||||
|
||||
### Install Pre-commit
|
||||
```bash
|
||||
pip install pre-commit
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
### Available Hooks
|
||||
- black (code formatting)
|
||||
- flake8 (linting)
|
||||
- isort (import sorting)
|
||||
- mypy (type checking)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Common Issues:
|
||||
1. Port already in use
|
||||
- Check for running processes: `lsof -i :8123`
|
||||
- Kill process if needed: `kill -9 PID`
|
||||
|
||||
2. Database connection issues
|
||||
- Verify PostgreSQL is running
|
||||
- Check connection settings in .env
|
||||
|
||||
3. Virtual environment problems
|
||||
- Delete and recreate: `rm -rf .venv && python -m venv .venv`
|
||||
- Reinstall dependencies
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Review the [Architecture Guide](../architecture.md)
|
||||
2. Check [Contributing Guidelines](../contributing.md)
|
||||
3. Start with [Simple Issues](https://github.com/jango-blockchained/homeassistant-mcp/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
|
||||
54
docs/development/index.md
Normal file
54
docs/development/index.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Development Guide
|
||||
|
||||
Welcome to the development guide for the Home Assistant MCP Server. This section provides comprehensive information for developers who want to contribute to or extend the project.
|
||||
|
||||
## Development Overview
|
||||
|
||||
The MCP Server is built with modern development practices in mind, focusing on:
|
||||
|
||||
- Clean, maintainable code
|
||||
- Comprehensive testing
|
||||
- Clear documentation
|
||||
- Modular architecture
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Set up your development environment
|
||||
2. Fork the repository
|
||||
3. Install dependencies
|
||||
4. Run tests
|
||||
5. Make your changes
|
||||
6. Submit a pull request
|
||||
|
||||
## Development Topics
|
||||
|
||||
- [Architecture](../architecture.md) - System architecture and design
|
||||
- [Contributing](../contributing.md) - Contribution guidelines
|
||||
- [Testing](../testing.md) - Testing framework and guidelines
|
||||
- [Troubleshooting](../troubleshooting.md) - Common issues and solutions
|
||||
- [Deployment](../deployment.md) - Deployment procedures
|
||||
- [Roadmap](../roadmap.md) - Future development plans
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Follow the coding style guide
|
||||
- Write comprehensive tests
|
||||
- Document your changes
|
||||
- Keep commits atomic
|
||||
- Use meaningful commit messages
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. Create a feature branch
|
||||
2. Make your changes
|
||||
3. Run tests
|
||||
4. Update documentation
|
||||
5. Submit a pull request
|
||||
6. Address review comments
|
||||
7. Merge when approved
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Review the [Architecture](../architecture.md)
|
||||
- Check [Contributing Guidelines](../contributing.md)
|
||||
- Set up your [Development Environment](environment.md)
|
||||
212
docs/features/speech.md
Normal file
212
docs/features/speech.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# Speech Features
|
||||
|
||||
The Home Assistant MCP Server includes powerful speech processing capabilities powered by fast-whisper and custom wake word detection. This guide explains how to set up and use these features effectively.
|
||||
|
||||
## Overview
|
||||
|
||||
The speech processing system consists of two main components:
|
||||
1. Wake Word Detection - Listens for specific trigger phrases
|
||||
2. Speech-to-Text - Transcribes spoken commands using fast-whisper
|
||||
|
||||
## Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. Docker environment:
|
||||
```bash
|
||||
docker --version # Should be 20.10.0 or higher
|
||||
```
|
||||
|
||||
2. For GPU acceleration:
|
||||
- NVIDIA GPU with CUDA support
|
||||
- NVIDIA Container Toolkit installed
|
||||
- NVIDIA drivers 450.80.02 or higher
|
||||
|
||||
### Installation
|
||||
|
||||
1. Enable speech features in your `.env`:
|
||||
```bash
|
||||
ENABLE_SPEECH_FEATURES=true
|
||||
ENABLE_WAKE_WORD=true
|
||||
ENABLE_SPEECH_TO_TEXT=true
|
||||
```
|
||||
|
||||
2. Configure model settings:
|
||||
```bash
|
||||
WHISPER_MODEL_PATH=/models
|
||||
WHISPER_MODEL_TYPE=base
|
||||
WHISPER_LANGUAGE=en
|
||||
WHISPER_TASK=transcribe
|
||||
WHISPER_DEVICE=cuda # or cpu
|
||||
```
|
||||
|
||||
3. Start the services:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Wake Word Detection
|
||||
|
||||
The wake word detector continuously listens for configured trigger phrases. Default wake words:
|
||||
- "hey jarvis"
|
||||
- "ok google"
|
||||
- "alexa"
|
||||
|
||||
Custom wake words can be configured:
|
||||
```bash
|
||||
WAKE_WORDS=computer,jarvis,assistant
|
||||
```
|
||||
|
||||
When a wake word is detected:
|
||||
1. The system starts recording audio
|
||||
2. Audio is processed through the speech-to-text pipeline
|
||||
3. The resulting command is processed by the server
|
||||
|
||||
### Speech-to-Text
|
||||
|
||||
#### Automatic Transcription
|
||||
|
||||
After wake word detection:
|
||||
1. Audio is automatically captured (default: 5 seconds)
|
||||
2. The audio is transcribed using the configured whisper model
|
||||
3. The transcribed text is processed as a command
|
||||
|
||||
#### Manual Transcription
|
||||
|
||||
You can also manually transcribe audio using the API:
|
||||
|
||||
```typescript
|
||||
// Using the TypeScript client
|
||||
import { SpeechService } from '@ha-mcp/client';
|
||||
|
||||
const speech = new SpeechService();
|
||||
|
||||
// Transcribe from audio buffer
|
||||
const buffer = await getAudioBuffer();
|
||||
const text = await speech.transcribe(buffer);
|
||||
|
||||
// Transcribe from file
|
||||
const text = await speech.transcribeFile('command.wav');
|
||||
```
|
||||
|
||||
```javascript
|
||||
// Using the REST API
|
||||
POST /api/speech/transcribe
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
file: <audio file>
|
||||
```
|
||||
|
||||
### Event Handling
|
||||
|
||||
The system emits various events during speech processing:
|
||||
|
||||
```typescript
|
||||
speech.on('wakeWord', (word: string) => {
|
||||
console.log(`Wake word detected: ${word}`);
|
||||
});
|
||||
|
||||
speech.on('listening', () => {
|
||||
console.log('Listening for command...');
|
||||
});
|
||||
|
||||
speech.on('transcribing', () => {
|
||||
console.log('Processing speech...');
|
||||
});
|
||||
|
||||
speech.on('transcribed', (text: string) => {
|
||||
console.log(`Transcribed text: ${text}`);
|
||||
});
|
||||
|
||||
speech.on('error', (error: Error) => {
|
||||
console.error('Speech processing error:', error);
|
||||
});
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Model Selection
|
||||
|
||||
Choose an appropriate model based on your needs:
|
||||
|
||||
1. Resource-constrained environments:
|
||||
- Use `tiny.en` or `base.en`
|
||||
- Run on CPU if GPU unavailable
|
||||
- Limit concurrent processing
|
||||
|
||||
2. High-accuracy requirements:
|
||||
- Use `small.en` or `medium.en`
|
||||
- Enable GPU acceleration
|
||||
- Increase audio quality
|
||||
|
||||
3. Production environments:
|
||||
- Use `base.en` or `small.en`
|
||||
- Enable GPU acceleration
|
||||
- Configure appropriate timeouts
|
||||
|
||||
### GPU Acceleration
|
||||
|
||||
When using GPU acceleration:
|
||||
|
||||
1. Monitor GPU memory usage:
|
||||
```bash
|
||||
nvidia-smi -l 1
|
||||
```
|
||||
|
||||
2. Adjust model size if needed:
|
||||
```bash
|
||||
WHISPER_MODEL_TYPE=small # Decrease if GPU memory limited
|
||||
```
|
||||
|
||||
3. Configure processing device:
|
||||
```bash
|
||||
WHISPER_DEVICE=cuda # Use GPU
|
||||
WHISPER_DEVICE=cpu # Use CPU if GPU unavailable
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. Wake word detection not working:
|
||||
- Check microphone permissions
|
||||
- Adjust `WAKE_WORD_SENSITIVITY`
|
||||
- Verify wake words configuration
|
||||
|
||||
2. Poor transcription quality:
|
||||
- Check audio input quality
|
||||
- Try a larger model
|
||||
- Verify language settings
|
||||
|
||||
3. Performance issues:
|
||||
- Monitor resource usage
|
||||
- Consider smaller model
|
||||
- Check GPU acceleration status
|
||||
|
||||
### Logging
|
||||
|
||||
Enable debug logging for detailed information:
|
||||
```bash
|
||||
LOG_LEVEL=debug
|
||||
```
|
||||
|
||||
Speech-specific logs will be tagged with `[SPEECH]` prefix.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. Audio Privacy:
|
||||
- Audio is processed locally
|
||||
- No data sent to external services
|
||||
- Temporary files automatically cleaned
|
||||
|
||||
2. Access Control:
|
||||
- Speech endpoints require authentication
|
||||
- Rate limiting applies to transcription
|
||||
- Configurable command restrictions
|
||||
|
||||
3. Resource Protection:
|
||||
- Timeouts prevent hanging
|
||||
- Memory limits enforced
|
||||
- Graceful error handling
|
||||
@@ -1,30 +0,0 @@
|
||||
# Getting Started
|
||||
|
||||
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](api/sse.md) for live communication.
|
||||
- **Tools:** Explore available [Tools](tools/tools.md) for device control and automation.
|
||||
- **Configuration:** Refer to the [Configuration Guide](getting-started/configuration.md) for setup and advanced settings.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter any issues:
|
||||
1. Verify that your Home Assistant instance is accessible.
|
||||
2. Ensure that all required environment variables are properly set.
|
||||
3. Consult the [Troubleshooting Guide](troubleshooting.md) for additional solutions.
|
||||
|
||||
## Development
|
||||
|
||||
For contributors:
|
||||
1. Fork the repository.
|
||||
2. Create a feature branch.
|
||||
3. Follow the [Development Guide](development/development.md) for contribution guidelines.
|
||||
4. Submit a pull request with your enhancements.
|
||||
|
||||
## Support
|
||||
|
||||
Need help?
|
||||
- Visit our [GitHub Issues](https://github.com/jango-blockchained/homeassistant-mcp/issues).
|
||||
- Review the [Troubleshooting Guide](troubleshooting.md).
|
||||
- Check the [FAQ](troubleshooting.md#faq) for common questions.
|
||||
8
docs/getting-started/index.md
Normal file
8
docs/getting-started/index.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Getting Started
|
||||
|
||||
Welcome to the Advanced Home Assistant MCP getting started guide. Follow these steps to begin:
|
||||
|
||||
1. [Installation](installation.md)
|
||||
2. [Configuration](configuration.md)
|
||||
3. [Docker Setup](docker.md)
|
||||
4. [Quick Start](quickstart.md)
|
||||
@@ -4,9 +4,18 @@ title: Home
|
||||
nav_order: 1
|
||||
---
|
||||
|
||||
# 🏠 MCP Server for Home Assistant
|
||||
# Advanced Home Assistant MCP
|
||||
|
||||
Welcome to the Model Context Protocol (MCP) Server documentation! This guide will help you get started with integrating a lightweight automation tool with your Home Assistant setup.
|
||||
Welcome to the Advanced Home Assistant Master Control Program documentation.
|
||||
|
||||
This documentation provides comprehensive information about setting up, configuring, and using the Advanced Home Assistant MCP system.
|
||||
|
||||
## Quick Links
|
||||
|
||||
- [Getting Started](getting-started/index.md)
|
||||
- [API Reference](api/index.md)
|
||||
- [Configuration Guide](getting-started/configuration.md)
|
||||
- [Docker Setup](getting-started/docker.md)
|
||||
|
||||
## What is MCP Server?
|
||||
|
||||
|
||||
62
docs/javascripts/extra.js
Normal file
62
docs/javascripts/extra.js
Normal file
@@ -0,0 +1,62 @@
|
||||
// Dark mode handling
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Check for saved dark mode preference
|
||||
const darkMode = localStorage.getItem('darkMode');
|
||||
if (darkMode === 'true') {
|
||||
document.body.classList.add('dark-mode');
|
||||
}
|
||||
});
|
||||
|
||||
// Smooth scrolling for anchor links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
document.querySelector(this.getAttribute('href')).scrollIntoView({
|
||||
behavior: 'smooth'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Add copy button to code blocks
|
||||
document.querySelectorAll('pre code').forEach((block) => {
|
||||
const button = document.createElement('button');
|
||||
button.className = 'copy-button';
|
||||
button.textContent = 'Copy';
|
||||
|
||||
button.addEventListener('click', async () => {
|
||||
await navigator.clipboard.writeText(block.textContent);
|
||||
button.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
button.textContent = 'Copy';
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
const pre = block.parentNode;
|
||||
pre.insertBefore(button, block);
|
||||
});
|
||||
|
||||
// Add version selector handling
|
||||
const versionSelector = document.querySelector('.version-selector');
|
||||
if (versionSelector) {
|
||||
versionSelector.addEventListener('change', (e) => {
|
||||
const version = e.target.value;
|
||||
window.location.href = `/${version}/`;
|
||||
});
|
||||
}
|
||||
|
||||
// Add feedback handling
|
||||
document.querySelectorAll('.feedback-button').forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
const feedback = this.getAttribute('data-feedback');
|
||||
// Send feedback to analytics
|
||||
if (typeof gtag !== 'undefined') {
|
||||
gtag('event', 'feedback', {
|
||||
'event_category': 'Documentation',
|
||||
'event_label': feedback
|
||||
});
|
||||
}
|
||||
// Show thank you message
|
||||
this.textContent = 'Thank you!';
|
||||
this.disabled = true;
|
||||
});
|
||||
});
|
||||
12
docs/javascripts/mathjax.js
Normal file
12
docs/javascripts/mathjax.js
Normal file
@@ -0,0 +1,12 @@
|
||||
window.MathJax = {
|
||||
tex: {
|
||||
inlineMath: [["\\(", "\\)"]],
|
||||
displayMath: [["\\[", "\\]"]],
|
||||
processEscapes: true,
|
||||
processEnvironments: true
|
||||
},
|
||||
options: {
|
||||
ignoreHtmlClass: ".*|",
|
||||
processHtmlClass: "arithmatex"
|
||||
}
|
||||
};
|
||||
42
docs/requirements.txt
Normal file
42
docs/requirements.txt
Normal file
@@ -0,0 +1,42 @@
|
||||
# Core
|
||||
mkdocs>=1.5.3
|
||||
mkdocs-material>=9.5.3
|
||||
|
||||
# Enhanced Functionality
|
||||
mkdocs-minify-plugin>=0.7.1
|
||||
mkdocs-git-revision-date-localized-plugin>=1.2.1
|
||||
mkdocs-glightbox>=0.3.4
|
||||
mkdocs-git-authors-plugin>=0.7.2
|
||||
mkdocs-git-committers-plugin>=0.2.3
|
||||
mkdocs-static-i18n>=1.2.0
|
||||
mkdocs-awesome-pages-plugin>=2.9.2
|
||||
mkdocs-redirects>=1.2.1
|
||||
mkdocs-include-markdown-plugin>=6.0.4
|
||||
mkdocs-macros-plugin>=1.0.4
|
||||
mkdocs-meta-descriptions-plugin>=3.0.0
|
||||
mkdocs-print-site-plugin>=2.3.6
|
||||
|
||||
# Code Documentation
|
||||
mkdocstrings>=0.24.0
|
||||
mkdocstrings-python>=1.7.5
|
||||
|
||||
# Markdown Extensions
|
||||
pymdown-extensions>=10.5
|
||||
markdown>=3.5.1
|
||||
mdx_truly_sane_lists>=1.3
|
||||
pygments>=2.17.2
|
||||
|
||||
# Math Support
|
||||
python-markdown-math>=0.8
|
||||
|
||||
# Diagrams
|
||||
plantuml-markdown>=3.9.2
|
||||
mkdocs-mermaid2-plugin>=1.1.1
|
||||
|
||||
# Search Enhancements
|
||||
mkdocs-material[imaging]>=9.5.3
|
||||
pillow>=10.2.0
|
||||
cairosvg>=2.7.1
|
||||
|
||||
# Development Tools
|
||||
mike>=2.0.0 # For version management
|
||||
146
docs/security.md
Normal file
146
docs/security.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# Security Guide
|
||||
|
||||
This document outlines security best practices and configurations for the Home Assistant MCP Server.
|
||||
|
||||
## Authentication
|
||||
|
||||
### JWT Authentication
|
||||
The server uses JWT (JSON Web Tokens) for API authentication:
|
||||
|
||||
```http
|
||||
Authorization: Bearer YOUR_JWT_TOKEN
|
||||
```
|
||||
|
||||
### Token Configuration
|
||||
```yaml
|
||||
security:
|
||||
jwt_secret: YOUR_SECRET_KEY
|
||||
token_expiry: 24h
|
||||
refresh_token_expiry: 7d
|
||||
```
|
||||
|
||||
## Access Control
|
||||
|
||||
### CORS Configuration
|
||||
Configure allowed origins to prevent unauthorized access:
|
||||
|
||||
```yaml
|
||||
security:
|
||||
allowed_origins:
|
||||
- http://localhost:3000
|
||||
- https://your-domain.com
|
||||
```
|
||||
|
||||
### IP Filtering
|
||||
Restrict access by IP address:
|
||||
|
||||
```yaml
|
||||
security:
|
||||
allowed_ips:
|
||||
- 192.168.1.0/24
|
||||
- 10.0.0.0/8
|
||||
```
|
||||
|
||||
## SSL/TLS Configuration
|
||||
|
||||
### Enable HTTPS
|
||||
```yaml
|
||||
ssl:
|
||||
enabled: true
|
||||
cert_file: /path/to/cert.pem
|
||||
key_file: /path/to/key.pem
|
||||
```
|
||||
|
||||
### Certificate Management
|
||||
1. Use Let's Encrypt for free SSL certificates
|
||||
2. Regularly renew certificates
|
||||
3. Monitor certificate expiration
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
### Basic Rate Limiting
|
||||
```yaml
|
||||
rate_limit:
|
||||
enabled: true
|
||||
requests_per_minute: 100
|
||||
burst: 20
|
||||
```
|
||||
|
||||
### Advanced Rate Limiting
|
||||
```yaml
|
||||
rate_limit:
|
||||
rules:
|
||||
- endpoint: /api/control
|
||||
requests_per_minute: 50
|
||||
- endpoint: /api/state
|
||||
requests_per_minute: 200
|
||||
```
|
||||
|
||||
## Data Protection
|
||||
|
||||
### Sensitive Data
|
||||
- Use environment variables for secrets
|
||||
- Encrypt sensitive data at rest
|
||||
- Implement secure backup procedures
|
||||
|
||||
### Logging Security
|
||||
- Avoid logging sensitive information
|
||||
- Rotate logs regularly
|
||||
- Protect log file access
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Regular Security Updates
|
||||
- Keep dependencies updated
|
||||
- Monitor security advisories
|
||||
- Apply patches promptly
|
||||
|
||||
2. Password Policies
|
||||
- Enforce strong passwords
|
||||
- Implement password expiration
|
||||
- Use secure password storage
|
||||
|
||||
3. Monitoring
|
||||
- Log security events
|
||||
- Monitor access patterns
|
||||
- Set up alerts for suspicious activity
|
||||
|
||||
4. Network Security
|
||||
- Use VPN for remote access
|
||||
- Implement network segmentation
|
||||
- Configure firewalls properly
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] Configure SSL/TLS
|
||||
- [ ] Set up JWT authentication
|
||||
- [ ] Configure CORS properly
|
||||
- [ ] Enable rate limiting
|
||||
- [ ] Implement IP filtering
|
||||
- [ ] Secure sensitive data
|
||||
- [ ] Set up monitoring
|
||||
- [ ] Configure backup encryption
|
||||
- [ ] Update security policies
|
||||
|
||||
## Incident Response
|
||||
|
||||
1. Detection
|
||||
- Monitor security logs
|
||||
- Set up intrusion detection
|
||||
- Configure alerts
|
||||
|
||||
2. Response
|
||||
- Document incident details
|
||||
- Isolate affected systems
|
||||
- Investigate root cause
|
||||
|
||||
3. Recovery
|
||||
- Apply security fixes
|
||||
- Restore from backups
|
||||
- Update security measures
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Security Best Practices](https://owasp.org/www-project-top-ten/)
|
||||
- [JWT Security](https://jwt.io/introduction)
|
||||
- [SSL Configuration](https://ssl-config.mozilla.org/)
|
||||
164
docs/stylesheets/extra.css
Normal file
164
docs/stylesheets/extra.css
Normal file
@@ -0,0 +1,164 @@
|
||||
/* Modern Dark Theme Enhancements */
|
||||
[data-md-color-scheme="slate"] {
|
||||
--md-default-bg-color: #1a1b26;
|
||||
--md-default-fg-color: #a9b1d6;
|
||||
--md-default-fg-color--light: #a9b1d6;
|
||||
--md-default-fg-color--lighter: #787c99;
|
||||
--md-default-fg-color--lightest: #4e5173;
|
||||
--md-primary-fg-color: #7aa2f7;
|
||||
--md-primary-fg-color--light: #7dcfff;
|
||||
--md-primary-fg-color--dark: #2ac3de;
|
||||
--md-accent-fg-color: #bb9af7;
|
||||
--md-accent-fg-color--transparent: #bb9af722;
|
||||
--md-accent-bg-color: #1a1b26;
|
||||
--md-accent-bg-color--light: #24283b;
|
||||
}
|
||||
|
||||
/* Code Blocks */
|
||||
.highlight pre {
|
||||
background-color: #24283b !important;
|
||||
border-radius: 6px;
|
||||
padding: 1em;
|
||||
margin: 1em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.highlight code {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Copy Button */
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
right: 0.5em;
|
||||
top: 0.5em;
|
||||
padding: 0.4em 0.8em;
|
||||
background-color: var(--md-accent-bg-color--light);
|
||||
border: 1px solid var(--md-accent-fg-color--transparent);
|
||||
border-radius: 4px;
|
||||
color: var(--md-default-fg-color);
|
||||
font-size: 0.8em;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.copy-button:hover {
|
||||
background-color: var(--md-accent-fg-color--transparent);
|
||||
border-color: var(--md-accent-fg-color);
|
||||
}
|
||||
|
||||
/* Navigation Enhancements */
|
||||
.md-nav {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.md-nav__link {
|
||||
padding: 0.4rem 0;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.md-nav__link:hover {
|
||||
color: var(--md-primary-fg-color) !important;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.md-tabs__link {
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.md-tabs__link:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.md-tabs__link--active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Admonitions */
|
||||
.md-typeset .admonition,
|
||||
.md-typeset details {
|
||||
border-width: 0;
|
||||
border-left-width: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.md-typeset table:not([class]) {
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 4px var(--md-accent-fg-color--transparent);
|
||||
}
|
||||
|
||||
.md-typeset table:not([class]) th {
|
||||
background-color: var(--md-accent-bg-color--light);
|
||||
border-bottom: 2px solid var(--md-accent-fg-color--transparent);
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.md-search__form {
|
||||
background-color: var(--md-accent-bg-color--light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Feedback Buttons */
|
||||
.feedback-button {
|
||||
padding: 0.5em 1em;
|
||||
margin: 0 0.5em;
|
||||
border-radius: 4px;
|
||||
background-color: var(--md-accent-bg-color--light);
|
||||
border: 1px solid var(--md-accent-fg-color--transparent);
|
||||
color: var(--md-default-fg-color);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.feedback-button:hover {
|
||||
background-color: var(--md-accent-fg-color--transparent);
|
||||
border-color: var(--md-accent-fg-color);
|
||||
}
|
||||
|
||||
.feedback-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Version Selector */
|
||||
.version-selector {
|
||||
padding: 0.5em;
|
||||
border-radius: 4px;
|
||||
background-color: var(--md-accent-bg-color--light);
|
||||
border: 1px solid var(--md-accent-fg-color--transparent);
|
||||
color: var(--md-default-fg-color);
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--md-accent-bg-color--light);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--md-accent-fg-color--transparent);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--md-accent-fg-color);
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.md-typeset a {
|
||||
color: var(--md-default-fg-color) !important;
|
||||
}
|
||||
|
||||
.md-content__inner {
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
42
docs/tools/index.md
Normal file
42
docs/tools/index.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Tools Overview
|
||||
|
||||
The Home Assistant MCP Server provides a variety of tools to help you manage and interact with your home automation system.
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Device Management
|
||||
- [List Devices](device-management/list-devices.md) - View and manage connected devices
|
||||
- [Device Control](device-management/control.md) - Control device states and settings
|
||||
|
||||
### History & State
|
||||
- [History](history-state/history.md) - View and analyze historical data
|
||||
- [Scene Management](history-state/scene.md) - Create and manage scenes
|
||||
|
||||
### Automation
|
||||
- [Automation Management](automation/automation.md) - Create and manage automations
|
||||
- [Automation Configuration](automation/automation-config.md) - Configure automation settings
|
||||
|
||||
### Add-ons & Packages
|
||||
- [Add-on Management](addons-packages/addon.md) - Manage server add-ons
|
||||
- [Package Management](addons-packages/package.md) - Handle package installations
|
||||
|
||||
### Notifications
|
||||
- [Notify](notifications/notify.md) - Send and manage notifications
|
||||
|
||||
### Events
|
||||
- [Event Subscription](events/subscribe-events.md) - Subscribe to system events
|
||||
- [SSE Statistics](events/sse-stats.md) - Monitor Server-Sent Events statistics
|
||||
|
||||
## Getting Started
|
||||
|
||||
To get started with these tools:
|
||||
|
||||
1. Ensure you have the MCP Server properly installed and configured
|
||||
2. Check the specific tool documentation for detailed usage instructions
|
||||
3. Use the API endpoints or command-line interface as needed
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Review the [API Documentation](../api/index.md) for programmatic access
|
||||
- Check [Configuration](../config/index.md) for tool-specific settings
|
||||
- See [Examples](../examples/index.md) for practical use cases
|
||||
@@ -1,127 +0,0 @@
|
||||
# Home Assistant MCP Tools
|
||||
|
||||
This section documents all available tools in the Home Assistant MCP.
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Device Management
|
||||
|
||||
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](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-state/history.md)
|
||||
- Fetch device state history
|
||||
- Filter by time range
|
||||
- Get significant changes
|
||||
|
||||
2. [Scene Management](history-state/scene.md)
|
||||
- List available scenes
|
||||
- Activate scenes
|
||||
- Scene state information
|
||||
|
||||
### Automation
|
||||
|
||||
1. [Automation Management](automation/automation.md)
|
||||
- List automations
|
||||
- Toggle automation state
|
||||
- Trigger automations manually
|
||||
|
||||
2. [Automation Configuration](automation/automation-config.md)
|
||||
- Create new automations
|
||||
- Update existing automations
|
||||
- Delete automations
|
||||
- Duplicate automations
|
||||
|
||||
### Add-ons and Packages
|
||||
|
||||
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](addons-packages/package.md)
|
||||
- Manage HACS packages
|
||||
- Install/update/remove packages
|
||||
- List available packages by category
|
||||
|
||||
### Notifications
|
||||
|
||||
1. [Notify](notifications/notify.md)
|
||||
- Send notifications
|
||||
- Support for multiple notification services
|
||||
- Custom notification data
|
||||
|
||||
### Real-time Events
|
||||
|
||||
1. [Event Subscription](events/subscribe-events.md)
|
||||
- Subscribe to Home Assistant events
|
||||
- Monitor specific entities
|
||||
- Domain-based monitoring
|
||||
|
||||
2. [SSE Statistics](events/sse-stats.md)
|
||||
- Get SSE connection statistics
|
||||
- Monitor active subscriptions
|
||||
- Connection management
|
||||
|
||||
## Using Tools
|
||||
|
||||
All tools can be accessed through:
|
||||
|
||||
1. REST API endpoints
|
||||
2. WebSocket connections
|
||||
3. Server-Sent Events (SSE)
|
||||
|
||||
### Authentication
|
||||
|
||||
Tools require authentication using:
|
||||
- Home Assistant Long-Lived Access Token
|
||||
- JWT tokens for specific operations
|
||||
|
||||
### Error Handling
|
||||
|
||||
All tools follow a consistent error handling pattern:
|
||||
```typescript
|
||||
{
|
||||
success: boolean;
|
||||
message?: string;
|
||||
data?: any;
|
||||
}
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Tools are subject to rate limiting:
|
||||
- Default: 100 requests per 15 minutes
|
||||
- Configurable through environment variables
|
||||
|
||||
## Tool Development
|
||||
|
||||
Want to create a new tool? Check out:
|
||||
- [Tool Development Guide](../development/tools.md)
|
||||
- [Tool Interface Documentation](../development/interfaces.md)
|
||||
- [Best Practices](../development/best-practices.md)
|
||||
|
||||
## Examples
|
||||
|
||||
Each tool documentation includes:
|
||||
- Usage examples
|
||||
- Code snippets
|
||||
- Common use cases
|
||||
- Troubleshooting tips
|
||||
|
||||
## Support
|
||||
|
||||
Need help with tools?
|
||||
- Check individual tool documentation
|
||||
- See [Troubleshooting Guide](../troubleshooting.md)
|
||||
- Create an issue on GitHub
|
||||
255
mkdocs.yml
255
mkdocs.yml
@@ -1,76 +1,220 @@
|
||||
site_name: Advanced Home Assistant MCP
|
||||
site_url: https://jango-blockchained.github.io/advanced-homeassitant-mcp
|
||||
repo_url: https://github.com/jango-blockchained/advanced-homeassitant-mcp
|
||||
site_name: MCP Server for Home Assistant
|
||||
site_url: https://jango-blockchained.github.io/advanced-homeassistant-mcp
|
||||
repo_url: https://github.com/jango-blockchained/advanced-homeassistant-mcp
|
||||
site_description: Home Assistant MCP Server Documentation
|
||||
# Add this to handle GitHub Pages serving from a subdirectory
|
||||
site_dir: site/advanced-homeassistant-mcp
|
||||
|
||||
theme:
|
||||
name: material
|
||||
logo: assets/images/logo.png
|
||||
favicon: assets/images/favicon.ico
|
||||
palette:
|
||||
- scheme: default
|
||||
primary: indigo
|
||||
accent: indigo
|
||||
toggle:
|
||||
icon: material/brightness-7
|
||||
name: Switch to dark mode
|
||||
- scheme: slate
|
||||
primary: indigo
|
||||
accent: indigo
|
||||
toggle:
|
||||
icon: material/brightness-4
|
||||
name: Switch to light mode
|
||||
|
||||
# Modern Features
|
||||
features:
|
||||
- navigation.instant
|
||||
- navigation.tracking
|
||||
# Navigation Enhancements
|
||||
- navigation.tabs
|
||||
- navigation.tabs.sticky
|
||||
- navigation.indexes
|
||||
- navigation.sections
|
||||
- navigation.expand
|
||||
- navigation.top
|
||||
- navigation.path
|
||||
- navigation.footer
|
||||
- navigation.prune
|
||||
- navigation.tracking
|
||||
- navigation.instant
|
||||
|
||||
# UI Elements
|
||||
- header.autohide
|
||||
- toc.integrate
|
||||
- toc.follow
|
||||
- announce.dismiss
|
||||
|
||||
# Search Features
|
||||
- search.suggest
|
||||
- search.highlight
|
||||
- search.share
|
||||
|
||||
# Code Features
|
||||
- content.code.annotate
|
||||
- content.code.copy
|
||||
- content.code.select
|
||||
- content.tabs.link
|
||||
- content.tooltips
|
||||
|
||||
# Theme Configuration
|
||||
palette:
|
||||
# Dark mode as primary
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: slate
|
||||
primary: deep-purple
|
||||
accent: purple
|
||||
toggle:
|
||||
icon: material/weather-sunny
|
||||
name: Switch to light mode
|
||||
# Light mode as secondary
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: default
|
||||
primary: deep-purple
|
||||
accent: purple
|
||||
toggle:
|
||||
icon: material/weather-night
|
||||
name: Switch to dark mode
|
||||
|
||||
font:
|
||||
text: Roboto
|
||||
code: Roboto Mono
|
||||
|
||||
icon:
|
||||
repo: fontawesome/brands/github
|
||||
edit: material/pencil
|
||||
view: material/eye
|
||||
|
||||
markdown_extensions:
|
||||
# Modern Code Highlighting
|
||||
- pymdownx.highlight:
|
||||
anchor_linenums: true
|
||||
line_spans: __span
|
||||
pygments_lang_class: true
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.snippets
|
||||
- pymdownx.superfences
|
||||
- admonition
|
||||
|
||||
# Advanced Formatting
|
||||
- pymdownx.critic
|
||||
- pymdownx.caret
|
||||
- pymdownx.keys
|
||||
- pymdownx.mark
|
||||
- pymdownx.tilde
|
||||
|
||||
# Interactive Elements
|
||||
- pymdownx.details
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
- pymdownx.tasklist:
|
||||
custom_checkbox: true
|
||||
|
||||
# Diagrams & Formatting
|
||||
- pymdownx.superfences:
|
||||
custom_fences:
|
||||
- name: mermaid
|
||||
class: mermaid
|
||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||
- pymdownx.arithmatex:
|
||||
generic: true
|
||||
|
||||
# Additional Extensions
|
||||
- admonition
|
||||
- attr_list
|
||||
- md_in_html
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||
- footnotes
|
||||
- tables
|
||||
- def_list
|
||||
- abbr
|
||||
|
||||
plugins:
|
||||
- search
|
||||
- git-revision-date-localized:
|
||||
type: date
|
||||
- mkdocstrings:
|
||||
default_handler: python
|
||||
handlers:
|
||||
python:
|
||||
options:
|
||||
show_source: true
|
||||
# Core Plugins
|
||||
- search:
|
||||
separator: '[\s\-,:!=\[\]()"/]+|(?!\b)(?=[A-Z][a-z])|\.(?!\d)|&[lg]t;'
|
||||
- minify:
|
||||
minify_html: true
|
||||
- mkdocstrings
|
||||
|
||||
# Advanced Features
|
||||
- social:
|
||||
cards: false
|
||||
- tags
|
||||
- offline
|
||||
|
||||
# Version Management
|
||||
- git-revision-date-localized:
|
||||
enable_creation_date: true
|
||||
type: date
|
||||
|
||||
extra:
|
||||
# Consent Management
|
||||
consent:
|
||||
title: Cookie consent
|
||||
description: >-
|
||||
We use cookies to recognize your repeated visits and preferences, as well
|
||||
as to measure the effectiveness of our documentation and whether users
|
||||
find what they're searching for. With your consent, you're helping us to
|
||||
make our documentation better.
|
||||
actions:
|
||||
- accept
|
||||
- reject
|
||||
- manage
|
||||
|
||||
# Version Management
|
||||
version:
|
||||
provider: mike
|
||||
default: latest
|
||||
|
||||
# Social Links
|
||||
social:
|
||||
- icon: fontawesome/brands/github
|
||||
link: https://github.com/jango-blockchained/homeassistant-mcp
|
||||
- icon: fontawesome/brands/docker
|
||||
link: https://hub.docker.com/r/jangoblockchained/homeassistant-mcp
|
||||
|
||||
# Status Indicators
|
||||
status:
|
||||
new: Recently added
|
||||
deprecated: Deprecated
|
||||
beta: Beta
|
||||
|
||||
# Analytics
|
||||
analytics:
|
||||
provider: google
|
||||
property: !ENV GOOGLE_ANALYTICS_KEY
|
||||
feedback:
|
||||
title: Was this page helpful?
|
||||
ratings:
|
||||
- icon: material/emoticon-happy-outline
|
||||
name: This page was helpful
|
||||
data: 1
|
||||
note: >-
|
||||
Thanks for your feedback!
|
||||
- icon: material/emoticon-sad-outline
|
||||
name: This page could be improved
|
||||
data: 0
|
||||
note: >-
|
||||
Thanks for your feedback! Please consider creating an issue to help us improve.
|
||||
|
||||
extra_css:
|
||||
- stylesheets/extra.css
|
||||
|
||||
extra_javascript:
|
||||
- javascripts/mathjax.js
|
||||
- https://polyfill.io/v3/polyfill.min.js?features=es6
|
||||
- https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
|
||||
- javascripts/extra.js
|
||||
|
||||
copyright: Copyright © 2025 jango-blockchained
|
||||
|
||||
# Keep existing nav structure
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Usage: usage.md
|
||||
- API: api.md
|
||||
- Configuration:
|
||||
- Claude Desktop Config: claude_desktop_config.md
|
||||
- Cline Config: cline_config.md
|
||||
- Getting Started:
|
||||
- Overview: getting-started.md
|
||||
- Overview: getting-started/index.md
|
||||
- Installation: getting-started/installation.md
|
||||
- Quick Start: getting-started/quickstart.md
|
||||
- Configuration: getting-started/configuration.md
|
||||
- Docker Setup: getting-started/docker.md
|
||||
- Quick Start: getting-started/quickstart.md
|
||||
- API Reference:
|
||||
- Overview: api/index.md
|
||||
- Core API: api.md
|
||||
- Core API: api/core.md
|
||||
- SSE API: api/sse.md
|
||||
- Core Functions: api/core.md
|
||||
- API Documentation: api.md
|
||||
- Usage: usage.md
|
||||
- Configuration:
|
||||
- Overview: config/index.md
|
||||
- System Configuration: configuration.md
|
||||
- Security: security.md
|
||||
- Tools:
|
||||
- Overview: tools/tools.md
|
||||
- Overview: tools/index.md
|
||||
- Device Management:
|
||||
- List Devices: tools/device-management/list-devices.md
|
||||
- Device Control: tools/device-management/control.md
|
||||
@@ -89,32 +233,17 @@ nav:
|
||||
- Event Subscription: tools/events/subscribe-events.md
|
||||
- SSE Statistics: tools/events/sse-stats.md
|
||||
- Development:
|
||||
- Overview: development/development.md
|
||||
- Overview: development/index.md
|
||||
- Environment Setup: development/environment.md
|
||||
- Architecture: architecture.md
|
||||
- Contributing: contributing.md
|
||||
- Testing: testing.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
|
||||
- Test Migration Guide: development/test-migration-guide.md
|
||||
- Troubleshooting: troubleshooting.md
|
||||
- Deployment: deployment.md
|
||||
- Roadmap: roadmap.md
|
||||
- Examples:
|
||||
- Overview: examples/index.md
|
||||
- Roadmap: roadmap.md
|
||||
|
||||
extra:
|
||||
social:
|
||||
- icon: fontawesome/brands/github
|
||||
link: https://github.com/jango-blockchained/homeassistant-mcp
|
||||
- icon: fontawesome/brands/docker
|
||||
link: https://hub.docker.com/r/jangoblockchained/homeassistant-mcp
|
||||
analytics:
|
||||
provider: google
|
||||
property: !ENV GOOGLE_ANALYTICS_KEY
|
||||
|
||||
extra_css:
|
||||
- stylesheets/extra.css
|
||||
|
||||
extra_javascript:
|
||||
- javascripts/extra.js
|
||||
|
||||
copyright: Copyright © 2025 jango-blockchained
|
||||
@@ -55,7 +55,8 @@
|
||||
"husky": "^9.0.11",
|
||||
"prettier": "^3.2.5",
|
||||
"supertest": "^6.3.3",
|
||||
"uuid": "^11.0.5"
|
||||
"uuid": "^11.0.5",
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"engines": {
|
||||
"bun": ">=1.0.0"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
test audio content
|
||||
@@ -1,183 +1,256 @@
|
||||
import WebSocket from "ws";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
interface HassMessage {
|
||||
type: string;
|
||||
id?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface HassAuthMessage extends HassMessage {
|
||||
type: "auth";
|
||||
access_token: string;
|
||||
}
|
||||
|
||||
interface HassEventMessage extends HassMessage {
|
||||
type: "event";
|
||||
event: {
|
||||
event_type: string;
|
||||
data: any;
|
||||
};
|
||||
}
|
||||
|
||||
interface HassSubscribeMessage extends HassMessage {
|
||||
type: "subscribe_events";
|
||||
event_type?: string;
|
||||
}
|
||||
|
||||
interface HassUnsubscribeMessage extends HassMessage {
|
||||
type: "unsubscribe_events";
|
||||
subscription: number;
|
||||
}
|
||||
|
||||
interface HassResultMessage extends HassMessage {
|
||||
type: "result";
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class HassWebSocketClient extends EventEmitter {
|
||||
private ws: WebSocket | null = null;
|
||||
private messageId = 1;
|
||||
private authenticated = false;
|
||||
private messageId = 1;
|
||||
private subscriptions = new Map<number, (data: any) => void>();
|
||||
private url: string;
|
||||
private token: string;
|
||||
private reconnectAttempts = 0;
|
||||
private maxReconnectAttempts = 5;
|
||||
private reconnectDelay = 1000;
|
||||
private subscriptions = new Map<string, (data: any) => void>();
|
||||
private maxReconnectAttempts = 3;
|
||||
|
||||
constructor(
|
||||
private url: string,
|
||||
private token: string,
|
||||
private options: {
|
||||
autoReconnect?: boolean;
|
||||
maxReconnectAttempts?: number;
|
||||
reconnectDelay?: number;
|
||||
} = {},
|
||||
) {
|
||||
constructor(url: string, token: string) {
|
||||
super();
|
||||
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
|
||||
this.reconnectDelay = options.reconnectDelay || 1000;
|
||||
this.url = url;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public async connect(): Promise<void> {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
this.ws = new WebSocket(this.url);
|
||||
|
||||
this.ws.on("open", () => {
|
||||
this.ws.onopen = () => {
|
||||
this.emit('connect');
|
||||
this.authenticate();
|
||||
});
|
||||
|
||||
this.ws.on("message", (data: string) => {
|
||||
const message = JSON.parse(data);
|
||||
this.handleMessage(message);
|
||||
});
|
||||
|
||||
this.ws.on("close", () => {
|
||||
this.handleDisconnect();
|
||||
});
|
||||
|
||||
this.ws.on("error", (error) => {
|
||||
this.emit("error", error);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
this.once("auth_ok", () => {
|
||||
this.authenticated = true;
|
||||
this.reconnectAttempts = 0;
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
||||
this.once("auth_invalid", () => {
|
||||
reject(new Error("Authentication failed"));
|
||||
});
|
||||
this.ws.onclose = () => {
|
||||
this.authenticated = false;
|
||||
this.emit('disconnect');
|
||||
this.handleReconnect();
|
||||
};
|
||||
|
||||
this.ws.onerror = (event: WebSocket.ErrorEvent) => {
|
||||
this.emit('error', event);
|
||||
reject(event);
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event: WebSocket.MessageEvent) => {
|
||||
if (typeof event.data === 'string') {
|
||||
this.handleMessage(event.data);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private authenticate(): void {
|
||||
this.send({
|
||||
type: "auth",
|
||||
access_token: this.token,
|
||||
});
|
||||
public isConnected(): boolean {
|
||||
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
private handleMessage(message: any): void {
|
||||
switch (message.type) {
|
||||
case "auth_required":
|
||||
this.authenticate();
|
||||
break;
|
||||
case "auth_ok":
|
||||
this.emit("auth_ok");
|
||||
break;
|
||||
case "auth_invalid":
|
||||
this.emit("auth_invalid");
|
||||
break;
|
||||
case "event":
|
||||
this.handleEvent(message);
|
||||
break;
|
||||
case "result":
|
||||
this.emit(`result_${message.id}`, message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private handleEvent(message: any): void {
|
||||
const subscription = this.subscriptions.get(message.event.event_type);
|
||||
if (subscription) {
|
||||
subscription(message.event.data);
|
||||
}
|
||||
this.emit("event", message.event);
|
||||
}
|
||||
|
||||
private handleDisconnect(): void {
|
||||
this.authenticated = false;
|
||||
this.emit("disconnected");
|
||||
|
||||
if (
|
||||
this.options.autoReconnect &&
|
||||
this.reconnectAttempts < this.maxReconnectAttempts
|
||||
) {
|
||||
setTimeout(
|
||||
() => {
|
||||
this.reconnectAttempts++;
|
||||
this.connect().catch((error) => {
|
||||
this.emit("error", error);
|
||||
});
|
||||
},
|
||||
this.reconnectDelay * Math.pow(2, this.reconnectAttempts),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async subscribeEvents(
|
||||
eventType: string,
|
||||
callback: (data: any) => void,
|
||||
): Promise<number> {
|
||||
if (!this.authenticated) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
|
||||
const id = this.messageId++;
|
||||
this.subscriptions.set(eventType, callback);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.send({
|
||||
id,
|
||||
type: "subscribe_events",
|
||||
event_type: eventType,
|
||||
});
|
||||
|
||||
this.once(`result_${id}`, (message) => {
|
||||
if (message.success) {
|
||||
resolve(id);
|
||||
} else {
|
||||
reject(new Error(message.error?.message || "Subscription failed"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async unsubscribeEvents(subscription: number): Promise<void> {
|
||||
if (!this.authenticated) {
|
||||
throw new Error("Not authenticated");
|
||||
}
|
||||
|
||||
const id = this.messageId++;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.send({
|
||||
id,
|
||||
type: "unsubscribe_events",
|
||||
subscription,
|
||||
});
|
||||
|
||||
this.once(`result_${id}`, (message) => {
|
||||
if (message.success) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(message.error?.message || "Unsubscribe failed"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private send(message: any): void {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify(message));
|
||||
}
|
||||
public isAuthenticated(): boolean {
|
||||
return this.authenticated;
|
||||
}
|
||||
|
||||
public disconnect(): void {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
this.authenticated = false;
|
||||
}
|
||||
}
|
||||
|
||||
private authenticate(): void {
|
||||
const authMessage: HassAuthMessage = {
|
||||
type: "auth",
|
||||
access_token: this.token
|
||||
};
|
||||
this.send(authMessage);
|
||||
}
|
||||
|
||||
private handleMessage(data: string): void {
|
||||
try {
|
||||
const message = JSON.parse(data) as HassMessage;
|
||||
|
||||
switch (message.type) {
|
||||
case "auth_ok":
|
||||
this.authenticated = true;
|
||||
this.emit('authenticated', message);
|
||||
break;
|
||||
|
||||
case "auth_invalid":
|
||||
this.authenticated = false;
|
||||
this.emit('auth_failed', message);
|
||||
this.disconnect();
|
||||
break;
|
||||
|
||||
case "event":
|
||||
this.handleEvent(message as HassEventMessage);
|
||||
break;
|
||||
|
||||
case "result": {
|
||||
const resultMessage = message as HassResultMessage;
|
||||
if (resultMessage.success) {
|
||||
this.emit('result', resultMessage);
|
||||
} else {
|
||||
this.emit('error', new Error(resultMessage.error || 'Unknown error'));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
this.emit('error', new Error(`Unknown message type: ${message.type}`));
|
||||
}
|
||||
} catch (error) {
|
||||
this.emit('error', error);
|
||||
}
|
||||
}
|
||||
|
||||
private handleEvent(message: HassEventMessage): void {
|
||||
this.emit('event', message.event);
|
||||
const callback = this.subscriptions.get(message.id || 0);
|
||||
if (callback) {
|
||||
callback(message.event.data);
|
||||
}
|
||||
}
|
||||
|
||||
public async subscribeEvents(eventType: string | undefined, callback: (data: any) => void): Promise<number> {
|
||||
if (!this.authenticated) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const id = this.messageId++;
|
||||
const message: HassSubscribeMessage = {
|
||||
id,
|
||||
type: "subscribe_events",
|
||||
event_type: eventType
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const handleResult = (result: HassResultMessage) => {
|
||||
if (result.id === id) {
|
||||
this.removeListener('result', handleResult);
|
||||
this.removeListener('error', handleError);
|
||||
|
||||
if (result.success) {
|
||||
this.subscriptions.set(id, callback);
|
||||
resolve(id);
|
||||
} else {
|
||||
reject(new Error(result.error || 'Failed to subscribe'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = (error: Error) => {
|
||||
this.removeListener('result', handleResult);
|
||||
this.removeListener('error', handleError);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
this.on('result', handleResult);
|
||||
this.on('error', handleError);
|
||||
|
||||
this.send(message);
|
||||
});
|
||||
}
|
||||
|
||||
public async unsubscribeEvents(subscription: number): Promise<boolean> {
|
||||
if (!this.authenticated) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
const message: HassUnsubscribeMessage = {
|
||||
id: this.messageId++,
|
||||
type: "unsubscribe_events",
|
||||
subscription
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const handleResult = (result: HassResultMessage) => {
|
||||
if (result.id === message.id) {
|
||||
this.removeListener('result', handleResult);
|
||||
this.removeListener('error', handleError);
|
||||
|
||||
if (result.success) {
|
||||
this.subscriptions.delete(subscription);
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error(result.error || 'Failed to unsubscribe'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleError = (error: Error) => {
|
||||
this.removeListener('result', handleResult);
|
||||
this.removeListener('error', handleError);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
this.on('result', handleResult);
|
||||
this.on('error', handleError);
|
||||
|
||||
this.send(message);
|
||||
});
|
||||
}
|
||||
|
||||
private send(message: HassMessage): void {
|
||||
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
||||
throw new Error('WebSocket is not connected');
|
||||
}
|
||||
this.ws.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
private handleReconnect(): void {
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.reconnectAttempts++;
|
||||
setTimeout(() => {
|
||||
this.connect().catch(() => { });
|
||||
}, 1000 * Math.pow(2, this.reconnectAttempts));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user