Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e96fa163cd | ||
|
|
cfef80e1e5 | ||
|
|
9b74a4354b | ||
|
|
fca193b5b2 | ||
|
|
cc9eede856 | ||
|
|
f0ff3d5e5a | ||
|
|
81d6dea7da | ||
|
|
1328bd1306 | ||
|
|
6fa88be433 | ||
|
|
2892f24030 | ||
|
|
1e3442db14 | ||
|
|
f74154d96f | ||
|
|
36d83e0a0e | ||
|
|
33defac76c | ||
|
|
4306a6866f | ||
|
|
039f6890a7 | ||
|
|
4fff318ea9 | ||
|
|
ea6efd553d | ||
|
|
d45ef5c622 | ||
|
|
9358f83229 | ||
|
|
e49d31d725 | ||
|
|
13a27e1d00 | ||
|
|
3e7f3920b2 | ||
|
|
8f8e3bd85e |
52
.github/workflows/deploy-docs.yml
vendored
52
.github/workflows/deploy-docs.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Deploy Documentation to GitHub Pages
|
name: Deploy Documentation
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -6,57 +6,69 @@ on:
|
|||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
- '.github/workflows/deploy-docs.yml'
|
- 'mkdocs.yml'
|
||||||
|
# Allow manual trigger
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pages: write
|
pages: write
|
||||||
id-token: write
|
id-token: write
|
||||||
|
|
||||||
# Allow only one concurrent deployment
|
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||||
concurrency:
|
concurrency:
|
||||||
group: "pages"
|
group: "pages"
|
||||||
cancel-in-progress: true
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Ruby
|
|
||||||
uses: ruby/setup-ruby@v1
|
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.2'
|
fetch-depth: 0
|
||||||
bundler-cache: true
|
|
||||||
cache-version: 0
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
cache: 'pip'
|
||||||
|
|
||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
uses: actions/configure-pages@v4
|
uses: actions/configure-pages@v4
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
cd docs
|
python -m pip install --upgrade pip
|
||||||
bundle install
|
pip install -r docs/requirements.txt
|
||||||
|
|
||||||
- name: Build site
|
- name: List mkdocs configuration
|
||||||
run: |
|
run: |
|
||||||
cd docs
|
echo "Current directory contents:"
|
||||||
bundle exec jekyll build
|
ls -la
|
||||||
env:
|
echo "MkDocs version:"
|
||||||
JEKYLL_ENV: production
|
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
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: docs/_site
|
path: ./site/advanced-homeassistant-mcp
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: build
|
|
||||||
environment:
|
environment:
|
||||||
name: github-pages
|
name: github-pages
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
|
|||||||
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
|
|
||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -31,7 +31,7 @@ wheels/
|
|||||||
venv/
|
venv/
|
||||||
ENV/
|
ENV/
|
||||||
env/
|
env/
|
||||||
|
.venv/
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
@@ -89,4 +89,13 @@ __pycache__/
|
|||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
models/
|
models/
|
||||||
|
|
||||||
|
*.code-workspace
|
||||||
|
*.ttf
|
||||||
|
*.otf
|
||||||
|
*.woff
|
||||||
|
*.woff2
|
||||||
|
*.eot
|
||||||
|
*.svg
|
||||||
|
*.png
|
||||||
361
README.md
361
README.md
@@ -1,305 +1,172 @@
|
|||||||
# 🚀 MCP Server for Home Assistant - Bringing AI-Powered Smart Homes to Life!
|
# MCP Server for Home Assistant 🏠🤖
|
||||||
|
|
||||||
[](LICENSE)
|
[](LICENSE) [](https://bun.sh) [](https://www.typescriptlang.org) [](https://smithery.ai/server/@jango-blockchained/advanced-homeassistant-mcp)
|
||||||
[](https://bun.sh)
|
|
||||||
[](https://www.typescriptlang.org)
|
|
||||||
[](#)
|
|
||||||
[](https://jango-blockchained.github.io/homeassistant-mcp/)
|
|
||||||
[](https://www.docker.com)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview 🌐
|
## Overview 🌐
|
||||||
|
|
||||||
Welcome to the **Model Context Protocol (MCP) Server for Home Assistant**! This robust platform bridges Home Assistant with cutting-edge Language Learning Models (LLMs), enabling natural language interactions and real-time automation of your smart devices. Imagine entering your home, saying:
|
MCP (Model Context Protocol) Server is a lightweight integration tool for Home Assistant, providing a flexible interface for device management and automation.
|
||||||
|
|
||||||
> "Hey MCP, dim the lights and start my evening playlist,"
|
## Core Features ✨
|
||||||
|
|
||||||
and watching your home transform instantly—that's the magic that MCP Server delivers!
|
- 🔌 Basic device control via REST API
|
||||||
|
- 📡 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 📋
|
||||||
|
|
||||||
## Key Benefits ✨
|
- 🚀 Bun runtime (v1.0.26+)
|
||||||
|
- 🏡 Home Assistant instance
|
||||||
|
- 🐳 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)
|
||||||
|
|
||||||
### 🎮 Device Control & Monitoring
|
## Installation 🛠️
|
||||||
- **Voice-Controlled Automation:**
|
|
||||||
Use simple commands like "Turn on the kitchen lights" or "Set the thermostat to 22°C" without touching a switch.
|
|
||||||
**Real-World Example:**
|
|
||||||
In the morning, say "Good morning! Open the blinds and start the coffee machine" to kickstart your day automatically.
|
|
||||||
|
|
||||||
- **Real-Time Communication:**
|
### Docker Deployment (Recommended)
|
||||||
Experience sub-100ms latency updates via Server-Sent Events (SSE) or WebSocket connections, ensuring your dashboard is always current.
|
|
||||||
**Real-World Example:**
|
|
||||||
Monitor energy usage instantly during peak hours and adjust remotely for efficient consumption.
|
|
||||||
|
|
||||||
- **Seamless Automation:**
|
|
||||||
Create scene-based rules to synchronize multiple devices effortlessly.
|
|
||||||
**Real-World Example:**
|
|
||||||
For movie nights, have MCP dim the lights, adjust the sound system, and launch your favorite streaming app with just one command.
|
|
||||||
|
|
||||||
### 🤖 AI-Powered Enhancements
|
|
||||||
- **Natural Language Processing (NLP):**
|
|
||||||
Convert everyday speech into actionable commands—just say, "Prepare the house for dinner," and MCP will adjust lighting, temperature, and even play soft background music.
|
|
||||||
|
|
||||||
- **Predictive Automation & Suggestions:**
|
|
||||||
Receive proactive recommendations based on usage habits and environmental trends.
|
|
||||||
**Real-World Example:**
|
|
||||||
When home temperature fluctuates unexpectedly, MCP suggests an optimal setting and notifies you immediately.
|
|
||||||
|
|
||||||
- **Anomaly Detection:**
|
|
||||||
Continuously monitor device activity and alert you to unusual behavior, helping prevent malfunctions or potential security breaches.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architectural Overview 🏗
|
|
||||||
|
|
||||||
Our architecture is engineered for performance, scalability, and security. The following Mermaid diagram illustrates the data flow and component interactions:
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
subgraph Client
|
|
||||||
A["Client Application (Web/Mobile/Voice)"]
|
|
||||||
end
|
|
||||||
subgraph CDN
|
|
||||||
B["CDN / Cache"]
|
|
||||||
end
|
|
||||||
subgraph Server
|
|
||||||
C["Bun Native Server"]
|
|
||||||
E["NLP Engine & Language Processing Module"]
|
|
||||||
end
|
|
||||||
subgraph Integration
|
|
||||||
D["Home Assistant (Devices, Lights, Thermostats)"]
|
|
||||||
end
|
|
||||||
|
|
||||||
A -->|HTTP Request| B
|
|
||||||
B -- Cache Miss --> C
|
|
||||||
C -->|Interpret Command| E
|
|
||||||
E -->|Determine Action| D
|
|
||||||
D -->|Return State/Action| C
|
|
||||||
C -->|Response| B
|
|
||||||
B -->|Cached/Processed Response| A
|
|
||||||
```
|
|
||||||
|
|
||||||
Learn more about our architecture in the [Architecture Documentation](docs/architecture.md).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Technical Stack 🔧
|
|
||||||
|
|
||||||
Our solution is built on a modern, high-performance stack that powers every feature:
|
|
||||||
|
|
||||||
- **Bun:**
|
|
||||||
A next-generation JavaScript runtime offering rapid startup times, native TypeScript support, and high performance.
|
|
||||||
👉 [Learn about Bun](https://bun.sh)
|
|
||||||
|
|
||||||
- **Bun Native Server:**
|
|
||||||
Utilizes Bun's built-in HTTP server to efficiently process API requests with sub-100ms response times.
|
|
||||||
👉 See the [Installation Guide](docs/getting-started/installation.md) for details.
|
|
||||||
|
|
||||||
- **Natural Language Processing (NLP) & LLM Integration:**
|
|
||||||
Processes and interprets natural language commands using state-of-the-art LLMs and custom NLP modules.
|
|
||||||
👉 Find API usage details in the [API Documentation](docs/api.md).
|
|
||||||
|
|
||||||
- **Home Assistant Integration:**
|
|
||||||
Provides seamless connectivity with Home Assistant, ensuring flawless communication with your smart devices.
|
|
||||||
👉 Refer to the [Usage Guide](docs/usage.md) for more information.
|
|
||||||
|
|
||||||
- **Redis Cache:**
|
|
||||||
Enables rapid data retrieval and session persistence essential for real-time updates.
|
|
||||||
|
|
||||||
- **TypeScript:**
|
|
||||||
Enhances type safety and developer productivity across the entire codebase.
|
|
||||||
|
|
||||||
- **JWT & Security Middleware:**
|
|
||||||
Protects your ecosystem with JWT-based authentication, request sanitization, rate-limiting, and encryption.
|
|
||||||
|
|
||||||
- **Containerization with Docker:**
|
|
||||||
Enables scalable, isolated deployments for production environments.
|
|
||||||
|
|
||||||
For further technical details, check out our [Documentation Index](docs/index.md).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Installation 🛠
|
|
||||||
|
|
||||||
### Installing via Smithery
|
|
||||||
|
|
||||||
To install Home Assistant MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@jango-blockchained/advanced-homeassistant-mcp):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx -y @smithery/cli install @jango-blockchained/advanced-homeassistant-mcp --client claude
|
# Clone the repository
|
||||||
```
|
git clone https://github.com/jango-blockchained/homeassistant-mcp.git
|
||||||
|
cd homeassistant-mcp
|
||||||
|
|
||||||
### 🐳 Docker Setup (Recommended)
|
# Copy and edit environment configuration
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your Home Assistant credentials and speech features settings
|
||||||
|
|
||||||
For a hassle-free, containerized deployment:
|
# Build and start containers
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Clone the repository (using a shallow copy for efficiency)
|
|
||||||
git clone --depth 1 https://github.com/jango-blockchained/homeassistant-mcp.git
|
|
||||||
|
|
||||||
# 2. Configure your environment: copy the example file and edit it with your Home Assistant credentials
|
|
||||||
cp .env.example .env # Modify .env with your Home Assistant host, tokens, etc.
|
|
||||||
|
|
||||||
# 3. Build and run the Docker containers
|
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
|
|
||||||
# 4. View real-time logs (last 50 log entries)
|
|
||||||
docker compose logs -f --tail=50
|
|
||||||
```
|
```
|
||||||
|
|
||||||
👉 Refer to our [Installation Guide](docs/getting-started/installation.md) for full details.
|
### Bare Metal Installation
|
||||||
|
|
||||||
### 💻 Bare Metal Installation
|
|
||||||
|
|
||||||
For direct deployment on your host machine:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Install Bun (if not already installed)
|
# Install Bun
|
||||||
curl -fsSL https://bun.sh/install | bash
|
curl -fsSL https://bun.sh/install | bash
|
||||||
|
|
||||||
# 2. Install project dependencies with caching support
|
# Clone the repository
|
||||||
bun install --frozen-lockfile
|
git clone https://github.com/jango-blockchained/homeassistant-mcp.git
|
||||||
|
cd homeassistant-mcp
|
||||||
|
|
||||||
# 3. Launch the server in development mode with hot-reload enabled
|
# Install dependencies
|
||||||
bun run dev --watch
|
bun install
|
||||||
|
|
||||||
|
# Start the server
|
||||||
|
bun run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
## Basic Usage 🖥️
|
||||||
|
|
||||||
## Real-World Usage Examples 🔍
|
### Device Control Example
|
||||||
|
|
||||||
### 📱 Smart Home Dashboard Integration
|
```typescript
|
||||||
Integrate MCP's real-time updates into your custom dashboard for a dynamic smart home experience:
|
// Turn on a light
|
||||||
|
const response = await fetch('http://localhost:3000/api/devices/light.living_room', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ state: 'on' })
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
```javascript
|
### WebSocket State Updates
|
||||||
const eventSource = new EventSource('http://localhost:3000/subscribe_events?token=YOUR_TOKEN&domain=light');
|
|
||||||
|
|
||||||
eventSource.onmessage = (event) => {
|
```typescript
|
||||||
const data = JSON.parse(event.data);
|
const ws = new WebSocket('ws://localhost:3000/devices');
|
||||||
console.log('Real-time update:', data);
|
ws.onmessage = (event) => {
|
||||||
// Update your UI dashboard, e.g., refresh a light intensity indicator.
|
const deviceState = JSON.parse(event.data);
|
||||||
|
console.log('Device state updated:', deviceState);
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🏠 Voice-Activated Control
|
## Speech Features (Optional)
|
||||||
Utilize voice commands to trigger actions with minimal effort:
|
|
||||||
|
|
||||||
```javascript
|
The MCP Server includes optional speech processing capabilities:
|
||||||
// Establish a WebSocket connection for real-time command processing
|
|
||||||
const ws = new WebSocket('wss://mcp.yourha.com/ws');
|
|
||||||
|
|
||||||
ws.onmessage = ({ data }) => {
|
### Prerequisites
|
||||||
const update = JSON.parse(data);
|
1. Docker installed and running
|
||||||
if (update.entity_id === 'light.living_room') {
|
2. NVIDIA GPU with CUDA support (optional)
|
||||||
console.log('Adjusting living room lighting based on voice command...');
|
3. At least 4GB RAM (8GB+ recommended for larger models)
|
||||||
// Additional logic to update your UI or trigger further actions can go here.
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Simulate processing a voice command
|
### Setup
|
||||||
function simulateVoiceCommand(command) {
|
|
||||||
console.log("Processing voice command:", command);
|
|
||||||
// Integrate with your actual voice-to-text system as needed.
|
|
||||||
}
|
|
||||||
|
|
||||||
simulateVoiceCommand("Turn off all the lights for bedtime");
|
|
||||||
```
|
|
||||||
|
|
||||||
👉 Learn more in our [Usage Guide](docs/usage.md).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Update Strategy 🔄
|
|
||||||
|
|
||||||
Maintain a seamless operation with zero downtime updates:
|
|
||||||
|
|
||||||
|
1. Enable speech features in your .env:
|
||||||
```bash
|
```bash
|
||||||
# 1. Pull the latest Docker images
|
ENABLE_SPEECH_FEATURES=true
|
||||||
docker compose pull
|
ENABLE_WAKE_WORD=true
|
||||||
|
ENABLE_SPEECH_TO_TEXT=true
|
||||||
# 2. Rebuild and restart containers smoothly
|
WHISPER_MODEL_PATH=/models
|
||||||
docker compose up -d --build
|
WHISPER_MODEL_TYPE=base
|
||||||
|
|
||||||
# 3. Clean up unused Docker images to free up space
|
|
||||||
docker system prune -f
|
|
||||||
```
|
```
|
||||||
|
|
||||||
For more details, review our [Troubleshooting & Updates](docs/troubleshooting.md).
|
2. Start the speech services:
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
---
|
### Available Models
|
||||||
|
|
||||||
## Security Features 🔐
|
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
|
||||||
|
|
||||||
We prioritize the security of your smart home with multiple layers of defense:
|
### Usage
|
||||||
- **JWT Authentication 🔑:** Secure, token-based API access to prevent unauthorized usage.
|
|
||||||
- **Request Sanitization 🧼:** Automatic filtering and validation of API requests to combat injection attacks.
|
|
||||||
- **Rate Limiting & Fail2Ban 🚫:** Monitors requests to prevent brute force and DDoS attacks.
|
|
||||||
- **End-to-End Encryption 🔒:** Ensures that your commands and data remain private during transmission.
|
|
||||||
|
|
||||||
---
|
1. Wake word detection listens for:
|
||||||
|
- "hey jarvis"
|
||||||
|
- "ok google"
|
||||||
|
- "alexa"
|
||||||
|
|
||||||
## Contributing 🤝
|
2. After wake word detection:
|
||||||
|
- Audio is automatically captured
|
||||||
|
- Speech is transcribed
|
||||||
|
- Commands are processed
|
||||||
|
|
||||||
We value community contributions! Here's how you can help improve MCP Server:
|
3. Manual transcription is also available:
|
||||||
1. **Fork the Repository 🍴**
|
```typescript
|
||||||
Create your own copy of the project.
|
const speech = speechService.getSpeechToText();
|
||||||
2. **Create a Feature Branch 🌿**
|
const text = await speech.transcribe(audioBuffer);
|
||||||
```bash
|
```
|
||||||
git checkout -b feature/your-feature-name
|
|
||||||
```
|
|
||||||
3. **Install Dependencies & Run Tests 🧪**
|
|
||||||
```bash
|
|
||||||
bun install
|
|
||||||
bun test --coverage
|
|
||||||
```
|
|
||||||
4. **Make Your Changes & Commit 📝**
|
|
||||||
Follow the [Conventional Commits](https://www.conventionalcommits.org) guidelines.
|
|
||||||
5. **Open a Pull Request 🔀**
|
|
||||||
Submit your changes for review.
|
|
||||||
|
|
||||||
Read more in our [Contribution Guidelines](docs/contributing.md).
|
## Configuration
|
||||||
|
|
||||||
---
|
See [Configuration Guide](docs/configuration.md) for detailed settings.
|
||||||
|
|
||||||
## Roadmap & Future Enhancements 🔮
|
## API Documentation
|
||||||
|
|
||||||
We're continuously evolving MCP Server. Upcoming features include:
|
See [API Documentation](docs/api/index.md) for available endpoints.
|
||||||
- **AI Assistant Integration (Q4 2024):**
|
|
||||||
Smarter, context-aware voice commands and personalized automation.
|
|
||||||
- **Predictive Automation (Q1 2025):**
|
|
||||||
Enhanced scheduling capabilities powered by advanced AI.
|
|
||||||
- **Enhanced Security (Q2 2024):**
|
|
||||||
Introduction of multi-factor authentication, advanced monitoring, and rigorous encryption methods.
|
|
||||||
- **Performance Optimizations (Q3 2024):**
|
|
||||||
Reducing latency further, optimizing caching, and improving load balancing.
|
|
||||||
|
|
||||||
For more details, see our [Roadmap](docs/roadmap.md).
|
## Development
|
||||||
|
|
||||||
---
|
See [Development Guide](docs/development/index.md) for contribution guidelines.
|
||||||
|
|
||||||
## Community & Support 🌍
|
## License 📄
|
||||||
|
|
||||||
Your feedback and collaboration are vital! Join our community:
|
MIT License. See [LICENSE](LICENSE) for details.
|
||||||
- **GitHub Issues:** Report bugs or request features via our [Issues Page](https://github.com/jango-blockchained/homeassistant-mcp/issues).
|
|
||||||
- **Discord & Slack:** Connect with fellow users and developers in real-time.
|
|
||||||
- **Documentation:** Find comprehensive guides on the [MCP Documentation Website](https://jango-blockchained.github.io/homeassistant-mcp/).
|
|
||||||
|
|
||||||
---
|
## Support 🆘
|
||||||
|
|
||||||
## License 📜
|
- 🐞 [GitHub Issues](https://github.com/jango-blockchained/homeassistant-mcp/issues)
|
||||||
|
- 📖 Documentation: [Project Docs](https://jango-blockchained.github.io/homeassistant-mcp/)
|
||||||
|
|
||||||
This project is licensed under the MIT License. See [LICENSE](LICENSE) for full details.
|
## MCP Client Integration 🔗
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
🔋 Batteries included.
|
|
||||||
|
|
||||||
## MCP Client Integration
|
|
||||||
|
|
||||||
This MCP server can be integrated with various clients that support the Model Context Protocol. Below are instructions for different client integrations:
|
This MCP server can be integrated with various clients that support the Model Context Protocol. Below are instructions for different client integrations:
|
||||||
|
|
||||||
### Cursor Integration
|
### Cursor Integration 🖱️
|
||||||
|
|
||||||
The server can be integrated with Cursor by adding the configuration to `.cursor/config/config.json`:
|
The server can be integrated with Cursor by adding the configuration to `.cursor/config/config.json`:
|
||||||
|
|
||||||
@@ -318,7 +185,7 @@ The server can be integrated with Cursor by adding the configuration to `.cursor
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Claude Desktop Integration
|
### Claude Desktop Integration 💬
|
||||||
|
|
||||||
For Claude Desktop, add the following to your Claude configuration file:
|
For Claude Desktop, add the following to your Claude configuration file:
|
||||||
|
|
||||||
@@ -336,7 +203,7 @@ For Claude Desktop, add the following to your Claude configuration file:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cline Integration
|
### Cline Integration 📟
|
||||||
|
|
||||||
For Cline-based clients, add the following configuration:
|
For Cline-based clients, add the following configuration:
|
||||||
|
|
||||||
@@ -361,7 +228,7 @@ For Cline-based clients, add the following configuration:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Command Line Usage
|
### Command Line Usage 💻
|
||||||
|
|
||||||
#### Windows
|
#### Windows
|
||||||
A CMD script is provided in the `scripts` directory. To use it:
|
A CMD script is provided in the `scripts` directory. To use it:
|
||||||
|
|||||||
@@ -1,149 +1,149 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test, beforeEach, afterEach, mock, spyOn } from "bun:test";
|
||||||
import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
|
|
||||||
import type { Mock } from "bun:test";
|
import type { Mock } from "bun:test";
|
||||||
import type { Express, Application } from 'express';
|
import type { Elysia } from "elysia";
|
||||||
import type { Logger } from 'winston';
|
|
||||||
|
|
||||||
// Types for our mocks
|
// Create mock instances
|
||||||
interface MockApp {
|
const mockApp = {
|
||||||
use: Mock<() => void>;
|
use: mock(() => mockApp),
|
||||||
listen: Mock<(port: number, callback: () => void) => { close: Mock<() => void> }>;
|
get: mock(() => mockApp),
|
||||||
}
|
post: mock(() => mockApp),
|
||||||
|
listen: mock((port: number, callback?: () => void) => {
|
||||||
interface MockLiteMCPInstance {
|
callback?.();
|
||||||
addTool: Mock<() => void>;
|
return mockApp;
|
||||||
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) };
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
const mockExpress = mock(() => mockApp);
|
|
||||||
|
|
||||||
// Mock LiteMCP instance
|
// Create mock constructors
|
||||||
const mockLiteMCPInstance: MockLiteMCPInstance = {
|
const MockElysia = mock(() => mockApp);
|
||||||
addTool: mock(() => undefined),
|
const mockCors = mock(() => (app: any) => app);
|
||||||
start: mock(() => Promise.resolve())
|
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
|
// Mock the modules
|
||||||
const mockLogger: MockLogger = {
|
const mockModules = {
|
||||||
info: mock((message: string) => undefined),
|
Elysia: MockElysia,
|
||||||
error: mock((message: string) => undefined),
|
cors: mockCors,
|
||||||
debug: mock((message: string) => undefined)
|
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', () => {
|
describe('Server Initialization', () => {
|
||||||
let originalEnv: NodeJS.ProcessEnv;
|
let originalEnv: NodeJS.ProcessEnv;
|
||||||
|
let consoleLog: Mock<typeof console.log>;
|
||||||
|
let consoleError: Mock<typeof console.error>;
|
||||||
|
let originalResolve: any;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Store original environment
|
// Store original environment
|
||||||
originalEnv = { ...process.env };
|
originalEnv = { ...process.env };
|
||||||
|
|
||||||
// Setup mocks
|
// Mock console methods
|
||||||
(globalThis as any).express = mockExpress;
|
consoleLog = mock(() => { });
|
||||||
(globalThis as any).LiteMCP = mockLiteMCP;
|
consoleError = mock(() => { });
|
||||||
(globalThis as any).logger = mockLogger;
|
console.log = consoleLog;
|
||||||
|
console.error = consoleError;
|
||||||
|
|
||||||
// Reset all mocks
|
// Reset all mocks
|
||||||
mockApp.use.mockReset();
|
for (const key in mockModules) {
|
||||||
mockApp.listen.mockReset();
|
const module = mockModules[key as keyof typeof mockModules];
|
||||||
mockLogger.info.mockReset();
|
if (typeof module === 'object' && module !== null) {
|
||||||
mockLogger.error.mockReset();
|
Object.values(module).forEach(value => {
|
||||||
mockLogger.debug.mockReset();
|
if (typeof value === 'function' && 'mock' in value) {
|
||||||
mockLiteMCP.mockReset();
|
(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(() => {
|
afterEach(() => {
|
||||||
// Restore original environment
|
// Restore original environment
|
||||||
process.env = originalEnv;
|
process.env = originalEnv;
|
||||||
|
|
||||||
// Clean up mocks
|
// Restore module resolution
|
||||||
delete (globalThis as any).express;
|
if (originalResolve) {
|
||||||
delete (globalThis as any).LiteMCP;
|
(globalThis as any).Bun.resolveSync = originalResolve;
|
||||||
delete (globalThis as any).logger;
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should start Express server when not in Claude mode', async () => {
|
test('should initialize server with middleware', async () => {
|
||||||
// Set OpenAI mode
|
// Import and initialize server
|
||||||
process.env.PROCESSOR_TYPE = 'openai';
|
const mod = await import('../src/index');
|
||||||
|
|
||||||
// Import the main module
|
// Verify server initialization
|
||||||
await import('../src/index.js');
|
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
|
// Verify console output
|
||||||
expect(mockExpress.mock.calls.length).toBeGreaterThan(0);
|
const logCalls = consoleLog.mock.calls;
|
||||||
expect(mockApp.use.mock.calls.length).toBeGreaterThan(0);
|
expect(logCalls.some(call =>
|
||||||
expect(mockApp.listen.mock.calls.length).toBeGreaterThan(0);
|
typeof call.args[0] === 'string' &&
|
||||||
|
call.args[0].includes('Server is running on port')
|
||||||
const infoMessages = mockLogger.info.mock.calls.map(([msg]) => msg);
|
)).toBe(true);
|
||||||
expect(infoMessages.some(msg => msg.includes('Server is running on port'))).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not start Express server in Claude mode', async () => {
|
test('should initialize speech service when enabled', async () => {
|
||||||
// Set Claude mode
|
// Enable speech service
|
||||||
process.env.PROCESSOR_TYPE = 'claude';
|
process.env.SPEECH_ENABLED = 'true';
|
||||||
|
|
||||||
// Import the main module
|
// Import and initialize server
|
||||||
await import('../src/index.js');
|
const mod = await import('../src/index');
|
||||||
|
|
||||||
// Verify Express server was not initialized
|
// Verify speech service initialization
|
||||||
expect(mockExpress.mock.calls.length).toBe(0);
|
expect(mockSpeechService.initialize.mock.calls.length).toBe(1);
|
||||||
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');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should initialize LiteMCP in both modes', async () => {
|
test('should handle server shutdown gracefully', async () => {
|
||||||
// Test OpenAI mode
|
// Enable speech service for shutdown test
|
||||||
process.env.PROCESSOR_TYPE = 'openai';
|
process.env.SPEECH_ENABLED = 'true';
|
||||||
await import('../src/index.js');
|
|
||||||
|
|
||||||
expect(mockLiteMCP.mock.calls.length).toBeGreaterThan(0);
|
// Import and initialize server
|
||||||
const [name, version] = mockLiteMCP.mock.calls[0] ?? [];
|
const mod = await import('../src/index');
|
||||||
expect(name).toBe('home-assistant');
|
|
||||||
expect(typeof version).toBe('string');
|
|
||||||
|
|
||||||
// Reset for next test
|
// Simulate SIGTERM
|
||||||
mockLiteMCP.mockReset();
|
process.emit('SIGTERM');
|
||||||
|
|
||||||
// Test Claude mode
|
// Verify shutdown behavior
|
||||||
process.env.PROCESSOR_TYPE = 'claude';
|
expect(mockSpeechService.shutdown.mock.calls.length).toBe(1);
|
||||||
await import('../src/index.js');
|
expect(consoleLog.mock.calls.some(call =>
|
||||||
|
typeof call.args[0] === 'string' &&
|
||||||
expect(mockLiteMCP.mock.calls.length).toBeGreaterThan(0);
|
call.args[0].includes('Shutting down gracefully')
|
||||||
const [name2, version2] = mockLiteMCP.mock.calls[0] ?? [];
|
)).toBe(true);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,81 +1,79 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test, beforeEach, afterEach, mock, spyOn } from "bun:test";
|
||||||
import { SpeechToText, TranscriptionResult, WakeWordEvent, TranscriptionError, TranscriptionOptions } from '../../src/speech/speechToText';
|
import type { Mock } from "bun:test";
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from "events";
|
||||||
import fs from 'fs';
|
import { SpeechToText, TranscriptionError, type TranscriptionOptions } from "../../src/speech/speechToText";
|
||||||
import path from 'path';
|
import type { SpeechToTextConfig } from "../../src/speech/types";
|
||||||
import { spawn } from 'child_process';
|
import type { ChildProcess } from "child_process";
|
||||||
import { describe, expect, beforeEach, afterEach, it, mock, spyOn } from 'bun:test';
|
|
||||||
|
|
||||||
// Mock child_process spawn
|
interface MockProcess extends EventEmitter {
|
||||||
const spawnMock = mock((cmd: string, args: string[]) => ({
|
stdout: EventEmitter;
|
||||||
stdout: new EventEmitter(),
|
stderr: EventEmitter;
|
||||||
stderr: new EventEmitter(),
|
kill: Mock<() => void>;
|
||||||
on: (event: string, cb: (code: number) => void) => {
|
}
|
||||||
if (event === 'close') setTimeout(() => cb(0), 0);
|
|
||||||
}
|
type SpawnFn = {
|
||||||
}));
|
(cmds: string[], options?: Record<string, unknown>): ChildProcess;
|
||||||
|
};
|
||||||
|
|
||||||
describe('SpeechToText', () => {
|
describe('SpeechToText', () => {
|
||||||
|
let spawnMock: Mock<SpawnFn>;
|
||||||
|
let mockProcess: MockProcess;
|
||||||
let speechToText: SpeechToText;
|
let speechToText: SpeechToText;
|
||||||
const testAudioDir = path.join(import.meta.dir, 'test_audio');
|
|
||||||
const mockConfig = {
|
|
||||||
containerName: 'test-whisper',
|
|
||||||
modelPath: '/models/whisper',
|
|
||||||
modelType: 'base.en'
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
speechToText = new SpeechToText(mockConfig);
|
// Create mock process
|
||||||
// Create test audio directory if it doesn't exist
|
mockProcess = new EventEmitter() as MockProcess;
|
||||||
if (!fs.existsSync(testAudioDir)) {
|
mockProcess.stdout = new EventEmitter();
|
||||||
fs.mkdirSync(testAudioDir, { recursive: true });
|
mockProcess.stderr = new EventEmitter();
|
||||||
}
|
mockProcess.kill = mock(() => { });
|
||||||
// Reset spawn mock
|
|
||||||
spawnMock.mockReset();
|
// 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(() => {
|
afterEach(() => {
|
||||||
speechToText.stopWakeWordDetection();
|
// Cleanup
|
||||||
// Clean up test files
|
mockProcess.removeAllListeners();
|
||||||
if (fs.existsSync(testAudioDir)) {
|
mockProcess.stdout.removeAllListeners();
|
||||||
fs.rmSync(testAudioDir, { recursive: true, force: true });
|
mockProcess.stderr.removeAllListeners();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Initialization', () => {
|
describe('Initialization', () => {
|
||||||
test('should create instance with default config', () => {
|
test('should create instance with default config', () => {
|
||||||
const instance = new SpeechToText({ modelPath: '/models/whisper', modelType: 'base.en' });
|
const config: SpeechToTextConfig = {
|
||||||
expect(instance instanceof EventEmitter).toBe(true);
|
modelPath: '/test/model',
|
||||||
expect(instance instanceof SpeechToText).toBe(true);
|
modelType: 'base.en'
|
||||||
|
};
|
||||||
|
const instance = new SpeechToText(config);
|
||||||
|
expect(instance).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should initialize successfully', async () => {
|
test('should initialize successfully', async () => {
|
||||||
const initSpy = spyOn(speechToText, 'initialize');
|
const result = await speechToText.initialize();
|
||||||
await speechToText.initialize();
|
expect(result).toBeUndefined();
|
||||||
expect(initSpy).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not initialize twice', async () => {
|
test('should not initialize twice', async () => {
|
||||||
await speechToText.initialize();
|
await speechToText.initialize();
|
||||||
const initSpy = spyOn(speechToText, 'initialize');
|
const result = await speechToText.initialize();
|
||||||
await speechToText.initialize();
|
expect(result).toBeUndefined();
|
||||||
expect(initSpy.mock.calls.length).toBe(1);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Health Check', () => {
|
describe('Health Check', () => {
|
||||||
test('should return true when Docker container is running', async () => {
|
test('should return true when Docker container is running', async () => {
|
||||||
const mockProcess = {
|
// Setup mock process
|
||||||
stdout: new EventEmitter(),
|
|
||||||
stderr: new EventEmitter(),
|
|
||||||
on: (event: string, cb: (code: number) => void) => {
|
|
||||||
if (event === 'close') setTimeout(() => cb(0), 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
spawnMock.mockImplementation(() => mockProcess);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
mockProcess.stdout.emtest('data', Buffer.from('Up 2 hours'));
|
mockProcess.stdout.emit('data', Buffer.from('Up 2 hours'));
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
const result = await speechToText.checkHealth();
|
const result = await speechToText.checkHealth();
|
||||||
@@ -83,23 +81,20 @@ describe('SpeechToText', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should return false when Docker container is not running', async () => {
|
test('should return false when Docker container is not running', async () => {
|
||||||
const mockProcess = {
|
// Setup mock process
|
||||||
stdout: new EventEmitter(),
|
setTimeout(() => {
|
||||||
stderr: new EventEmitter(),
|
mockProcess.stdout.emit('data', Buffer.from('No containers found'));
|
||||||
on: (event: string, cb: (code: number) => void) => {
|
}, 0);
|
||||||
if (event === 'close') setTimeout(() => cb(1), 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
spawnMock.mockImplementation(() => mockProcess);
|
|
||||||
|
|
||||||
const result = await speechToText.checkHealth();
|
const result = await speechToText.checkHealth();
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle Docker command errors', async () => {
|
test('should handle Docker command errors', async () => {
|
||||||
spawnMock.mockImplementation(() => {
|
// Setup mock process
|
||||||
throw new Error('Docker not found');
|
setTimeout(() => {
|
||||||
});
|
mockProcess.stderr.emit('data', Buffer.from('Docker error'));
|
||||||
|
}, 0);
|
||||||
|
|
||||||
const result = await speechToText.checkHealth();
|
const result = await speechToText.checkHealth();
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
@@ -108,51 +103,48 @@ describe('SpeechToText', () => {
|
|||||||
|
|
||||||
describe('Wake Word Detection', () => {
|
describe('Wake Word Detection', () => {
|
||||||
test('should detect wake word and emit event', async () => {
|
test('should detect wake word and emit event', async () => {
|
||||||
const testFile = path.join(testAudioDir, 'wake_word_test_123456.wav');
|
// Setup mock process
|
||||||
const testMetadata = `${testFile}.json`;
|
setTimeout(() => {
|
||||||
|
mockProcess.stdout.emit('data', Buffer.from('Wake word detected'));
|
||||||
|
}, 0);
|
||||||
|
|
||||||
return new Promise<void>((resolve) => {
|
const wakeWordPromise = new Promise<void>((resolve) => {
|
||||||
speechToText.startWakeWordDetection(testAudioDir);
|
speechToText.on('wake_word', () => {
|
||||||
|
|
||||||
speechToText.on('wake_word', (event: WakeWordEvent) => {
|
|
||||||
expect(event).toBeDefined();
|
|
||||||
expect(event.audioFile).toBe(testFile);
|
|
||||||
expect(event.metadataFile).toBe(testMetadata);
|
|
||||||
expect(event.timestamp).toBe('123456');
|
|
||||||
resolve();
|
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 () => {
|
test('should handle non-wake-word files', async () => {
|
||||||
const testFile = path.join(testAudioDir, 'regular_audio.wav');
|
// Setup mock process
|
||||||
let eventEmitted = false;
|
setTimeout(() => {
|
||||||
|
mockProcess.stdout.emit('data', Buffer.from('Processing audio'));
|
||||||
|
}, 0);
|
||||||
|
|
||||||
return new Promise<void>((resolve) => {
|
const wakeWordPromise = new Promise<void>((resolve, reject) => {
|
||||||
speechToText.startWakeWordDetection(testAudioDir);
|
const timeout = setTimeout(() => {
|
||||||
|
|
||||||
speechToText.on('wake_word', () => {
|
|
||||||
eventEmitted = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
fs.writeFileSync(testFile, 'test audio content');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
expect(eventEmitted).toBe(false);
|
|
||||||
resolve();
|
resolve();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
|
speechToText.on('wake_word', () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
reject(new Error('Wake word should not be detected'));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
speechToText.startWakeWordDetection();
|
||||||
|
await wakeWordPromise;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Audio Transcription', () => {
|
describe('Audio Transcription', () => {
|
||||||
const mockTranscriptionResult: TranscriptionResult = {
|
const mockTranscriptionResult = {
|
||||||
text: 'Hello world',
|
text: 'Test transcription',
|
||||||
segments: [{
|
segments: [{
|
||||||
text: 'Hello world',
|
text: 'Test transcription',
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 1,
|
end: 1,
|
||||||
confidence: 0.95
|
confidence: 0.95
|
||||||
@@ -160,169 +152,100 @@ describe('SpeechToText', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
test('should transcribe audio successfully', async () => {
|
test('should transcribe audio successfully', async () => {
|
||||||
const mockProcess = {
|
// Setup mock process
|
||||||
stdout: new EventEmitter(),
|
|
||||||
stderr: new EventEmitter(),
|
|
||||||
on: (event: string, cb: (code: number) => void) => {
|
|
||||||
if (event === 'close') setTimeout(() => cb(0), 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
spawnMock.mockImplementation(() => mockProcess);
|
|
||||||
|
|
||||||
const transcriptionPromise = speechToText.transcribeAudio('/test/audio.wav');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
mockProcess.stdout.emtest('data', Buffer.from(JSON.stringify(mockTranscriptionResult)));
|
mockProcess.stdout.emit('data', Buffer.from(JSON.stringify(mockTranscriptionResult)));
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
const result = await transcriptionPromise;
|
const result = await speechToText.transcribeAudio('/test/audio.wav');
|
||||||
expect(result).toEqual(mockTranscriptionResult);
|
expect(result).toEqual(mockTranscriptionResult);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle transcription errors', async () => {
|
test('should handle transcription errors', async () => {
|
||||||
const mockProcess = {
|
// Setup mock process
|
||||||
stdout: new EventEmitter(),
|
|
||||||
stderr: new EventEmitter(),
|
|
||||||
on: (event: string, cb: (code: number) => void) => {
|
|
||||||
if (event === 'close') setTimeout(() => cb(1), 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
spawnMock.mockImplementation(() => mockProcess);
|
|
||||||
|
|
||||||
const transcriptionPromise = speechToText.transcribeAudio('/test/audio.wav');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
mockProcess.stderr.emtest('data', Buffer.from('Transcription failed'));
|
mockProcess.stderr.emit('data', Buffer.from('Transcription failed'));
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
await expect(transcriptionPromise).rejects.toThrow(TranscriptionError);
|
await expect(speechToText.transcribeAudio('/test/audio.wav')).rejects.toThrow(TranscriptionError);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle invalid JSON output', async () => {
|
test('should handle invalid JSON output', async () => {
|
||||||
const mockProcess = {
|
// Setup mock process
|
||||||
stdout: new EventEmitter(),
|
|
||||||
stderr: new EventEmitter(),
|
|
||||||
on: (event: string, cb: (code: number) => void) => {
|
|
||||||
if (event === 'close') setTimeout(() => cb(0), 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
spawnMock.mockImplementation(() => mockProcess);
|
|
||||||
|
|
||||||
const transcriptionPromise = speechToText.transcribeAudio('/test/audio.wav');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
mockProcess.stdout.emtest('data', Buffer.from('Invalid JSON'));
|
mockProcess.stdout.emit('data', Buffer.from('Invalid JSON'));
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
await expect(transcriptionPromise).rejects.toThrow(TranscriptionError);
|
await expect(speechToText.transcribeAudio('/test/audio.wav')).rejects.toThrow(TranscriptionError);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should pass correct transcription options', async () => {
|
test('should pass correct transcription options', async () => {
|
||||||
const options: TranscriptionOptions = {
|
const options: TranscriptionOptions = {
|
||||||
model: 'large-v2',
|
model: 'base.en',
|
||||||
language: 'en',
|
language: 'en',
|
||||||
temperature: 0.5,
|
temperature: 0,
|
||||||
beamSize: 3,
|
beamSize: 5,
|
||||||
patience: 2,
|
patience: 1,
|
||||||
device: 'cuda'
|
device: 'cpu'
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockProcess = {
|
await speechToText.transcribeAudio('/test/audio.wav', options);
|
||||||
stdout: new EventEmitter(),
|
|
||||||
stderr: new EventEmitter(),
|
|
||||||
on: (event: string, cb: (code: number) => void) => {
|
|
||||||
if (event === 'close') setTimeout(() => cb(0), 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
spawnMock.mockImplementation(() => mockProcess);
|
|
||||||
|
|
||||||
const transcriptionPromise = speechToText.transcribeAudio('/test/audio.wav', options);
|
const spawnArgs = spawnMock.mock.calls[0]?.args[1] || [];
|
||||||
|
expect(spawnArgs).toContain('--model');
|
||||||
const expectedArgs = [
|
expect(spawnArgs).toContain(options.model);
|
||||||
'exec',
|
expect(spawnArgs).toContain('--language');
|
||||||
mockConfig.containerName,
|
expect(spawnArgs).toContain(options.language);
|
||||||
'fast-whisper',
|
expect(spawnArgs).toContain('--temperature');
|
||||||
'--model', options.model,
|
expect(spawnArgs).toContain(options.temperature?.toString());
|
||||||
'--language', options.language,
|
expect(spawnArgs).toContain('--beam-size');
|
||||||
'--temperature', String(options.temperature ?? 0),
|
expect(spawnArgs).toContain(options.beamSize?.toString());
|
||||||
'--beam-size', String(options.beamSize ?? 5),
|
expect(spawnArgs).toContain('--patience');
|
||||||
'--patience', String(options.patience ?? 1),
|
expect(spawnArgs).toContain(options.patience?.toString());
|
||||||
'--device', options.device
|
expect(spawnArgs).toContain('--device');
|
||||||
].filter((arg): arg is string => arg !== undefined);
|
expect(spawnArgs).toContain(options.device);
|
||||||
|
|
||||||
const mockCalls = spawnMock.mock.calls;
|
|
||||||
expect(mockCalls.length).toBe(1);
|
|
||||||
const [cmd, args] = mockCalls[0].args;
|
|
||||||
expect(cmd).toBe('docker');
|
|
||||||
expect(expectedArgs.every(arg => args.includes(arg))).toBe(true);
|
|
||||||
|
|
||||||
await transcriptionPromise.catch(() => { });
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Event Handling', () => {
|
describe('Event Handling', () => {
|
||||||
test('should emit progress events', async () => {
|
test('should emit progress events', async () => {
|
||||||
const mockProcess = {
|
const progressPromise = new Promise<void>((resolve) => {
|
||||||
stdout: new EventEmitter(),
|
speechToText.on('progress', (progress) => {
|
||||||
stderr: new EventEmitter(),
|
expect(progress).toEqual({ type: 'stdout', data: 'Processing' });
|
||||||
on: (event: string, cb: (code: number) => void) => {
|
resolve();
|
||||||
if (event === 'close') setTimeout(() => cb(0), 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
spawnMock.mockImplementation(() => mockProcess);
|
|
||||||
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
const progressEvents: any[] = [];
|
|
||||||
speechToText.on('progress', (event) => {
|
|
||||||
progressEvents.push(event);
|
|
||||||
if (progressEvents.length === 2) {
|
|
||||||
expect(progressEvents).toEqual([
|
|
||||||
{ type: 'stdout', data: 'Processing' },
|
|
||||||
{ type: 'stderr', data: 'Loading model' }
|
|
||||||
]);
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
void speechToText.transcribeAudio('/test/audio.wav');
|
|
||||||
|
|
||||||
mockProcess.stdout.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 () => {
|
test('should emit error events', async () => {
|
||||||
return new Promise<void>((resolve) => {
|
const errorPromise = new Promise<void>((resolve) => {
|
||||||
speechToText.on('error', (error) => {
|
speechToText.on('error', (error) => {
|
||||||
expect(error instanceof Error).toBe(true);
|
expect(error instanceof Error).toBe(true);
|
||||||
expect(error.message).toBe('Test error');
|
expect(error.message).toBe('Test error');
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
speechToText.emtest('error', new Error('Test error'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
speechToText.emit('error', new Error('Test error'));
|
||||||
|
await errorPromise;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Cleanup', () => {
|
describe('Cleanup', () => {
|
||||||
test('should stop wake word detection', () => {
|
test('should stop wake word detection', () => {
|
||||||
speechToText.startWakeWordDetection(testAudioDir);
|
speechToText.startWakeWordDetection();
|
||||||
speechToText.stopWakeWordDetection();
|
speechToText.stopWakeWordDetection();
|
||||||
// Verify no more file watching events are processed
|
expect(mockProcess.kill.mock.calls.length).toBe(1);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should clean up resources on shutdown', async () => {
|
test('should clean up resources on shutdown', async () => {
|
||||||
await speechToText.initialize();
|
await speechToText.initialize();
|
||||||
const shutdownSpy = spyOn(speechToText, 'shutdown');
|
|
||||||
await 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 { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
|
||||||
import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
import { EventEmitter } from "events";
|
||||||
import { HassWebSocketClient } from '../../src/websocket/client.js';
|
import { HassWebSocketClient } from "../../src/websocket/client";
|
||||||
import WebSocket from 'ws';
|
import type { MessageEvent, ErrorEvent } from "ws";
|
||||||
import { EventEmitter } from 'events';
|
import { Mock, fn as jestMock } from 'jest-mock';
|
||||||
import * as HomeAssistant from '../../src/types/hass.js';
|
import { expect as jestExpect } from '@jest/globals';
|
||||||
|
|
||||||
// Mock WebSocket
|
|
||||||
// // jest.mock('ws');
|
|
||||||
|
|
||||||
describe('WebSocket Event Handling', () => {
|
describe('WebSocket Event Handling', () => {
|
||||||
let client: HassWebSocketClient;
|
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;
|
let eventEmitter: EventEmitter;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Clear all mocks
|
|
||||||
jest.clearAllMocks();
|
|
||||||
|
|
||||||
// Create event emitter for mocking WebSocket events
|
|
||||||
eventEmitter = new EventEmitter();
|
eventEmitter = new EventEmitter();
|
||||||
|
|
||||||
// Create mock WebSocket instance
|
// Initialize callbacks first
|
||||||
|
onOpenCallback = () => { };
|
||||||
|
onCloseCallback = () => { };
|
||||||
|
onErrorCallback = () => { };
|
||||||
|
onMessageCallback = () => { };
|
||||||
|
|
||||||
mockWebSocket = {
|
mockWebSocket = {
|
||||||
on: jest.fn((event: string, listener: (...args: any[]) => void) => {
|
|
||||||
eventEmitter.on(event, listener);
|
|
||||||
return mockWebSocket;
|
|
||||||
}),
|
|
||||||
send: mock(),
|
send: mock(),
|
||||||
close: mock(),
|
close: mock(),
|
||||||
readyState: WebSocket.OPEN,
|
readyState: 1,
|
||||||
removeAllListeners: mock(),
|
OPEN: 1,
|
||||||
// Add required WebSocket properties
|
onopen: null,
|
||||||
binaryType: 'arraybuffer',
|
onclose: null,
|
||||||
bufferedAmount: 0,
|
onerror: null,
|
||||||
extensions: '',
|
onmessage: null
|
||||||
protocol: '',
|
};
|
||||||
url: 'ws://test.com',
|
|
||||||
isPaused: () => false,
|
|
||||||
ping: mock(),
|
|
||||||
pong: mock(),
|
|
||||||
terminate: mock()
|
|
||||||
} as unknown as jest.Mocked<WebSocket>;
|
|
||||||
|
|
||||||
// Mock WebSocket constructor
|
// Define setters that store the callbacks
|
||||||
(WebSocket as unknown as jest.Mock).mockImplementation(() => mockWebSocket);
|
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
|
// @ts-expect-error - Mock WebSocket implementation
|
||||||
client = new HassWebSocketClient('ws://test.com', 'test-token');
|
global.WebSocket = mock(() => mockWebSocket);
|
||||||
|
|
||||||
|
client = new HassWebSocketClient('ws://localhost:8123/api/websocket', 'test-token');
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
eventEmitter.removeAllListeners();
|
if (eventEmitter) {
|
||||||
client.disconnect();
|
eventEmitter.removeAllListeners();
|
||||||
|
}
|
||||||
|
if (client) {
|
||||||
|
client.disconnect();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle connection events', () => {
|
test('should handle connection events', async () => {
|
||||||
// Simulate open event
|
const connectPromise = client.connect();
|
||||||
eventEmitter.emtest('open');
|
onOpenCallback();
|
||||||
|
await connectPromise;
|
||||||
// Verify authentication message was sent
|
expect(client.isConnected()).toBe(true);
|
||||||
expect(mockWebSocket.send).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining('"type":"auth"')
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle authentication response', () => {
|
test('should handle authentication response', async () => {
|
||||||
// Simulate auth_ok message
|
const connectPromise = client.connect();
|
||||||
eventEmitter.emtest('message', JSON.stringify({ type: 'auth_ok' }));
|
onOpenCallback();
|
||||||
|
|
||||||
// Verify client is ready for commands
|
onMessageCallback({
|
||||||
expect(mockWebSocket.readyState).toBe(WebSocket.OPEN);
|
data: JSON.stringify({
|
||||||
|
type: 'auth_required'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
onMessageCallback({
|
||||||
|
data: JSON.stringify({
|
||||||
|
type: 'auth_ok'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
await connectPromise;
|
||||||
|
expect(client.isAuthenticated()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle auth failure', () => {
|
test('should handle auth failure', async () => {
|
||||||
// Simulate auth_invalid message
|
const connectPromise = client.connect();
|
||||||
eventEmitter.emtest('message', JSON.stringify({
|
onOpenCallback();
|
||||||
type: 'auth_invalid',
|
|
||||||
message: 'Invalid token'
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Verify client attempts to close connection
|
onMessageCallback({
|
||||||
expect(mockWebSocket.close).toHaveBeenCalled();
|
data: JSON.stringify({
|
||||||
|
type: 'auth_required'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
onMessageCallback({
|
||||||
|
data: JSON.stringify({
|
||||||
|
type: 'auth_invalid',
|
||||||
|
message: 'Invalid password'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(connectPromise).rejects.toThrow('Authentication failed');
|
||||||
|
expect(client.isAuthenticated()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle connection errors', () => {
|
test('should handle connection errors', async () => {
|
||||||
// Create error spy
|
const errorPromise = new Promise((resolve) => {
|
||||||
const errorSpy = mock();
|
client.on('error', resolve);
|
||||||
client.on('error', errorSpy);
|
});
|
||||||
|
|
||||||
// Simulate error
|
const connectPromise = client.connect().catch(() => { });
|
||||||
const testError = new Error('Test error');
|
onOpenCallback();
|
||||||
eventEmitter.emtest('error', testError);
|
|
||||||
|
|
||||||
// Verify error was handled
|
const errorEvent = {
|
||||||
expect(errorSpy).toHaveBeenCalledWith(testError);
|
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', () => {
|
test('should handle disconnection', async () => {
|
||||||
// Create close spy
|
const connectPromise = client.connect();
|
||||||
const closeSpy = mock();
|
onOpenCallback();
|
||||||
client.on('close', closeSpy);
|
await connectPromise;
|
||||||
|
|
||||||
// Simulate close
|
const disconnectPromise = new Promise((resolve) => {
|
||||||
eventEmitter.emtest('close');
|
client.on('disconnected', resolve);
|
||||||
|
});
|
||||||
|
|
||||||
// Verify close was handled
|
onCloseCallback();
|
||||||
expect(closeSpy).toHaveBeenCalled();
|
|
||||||
|
await disconnectPromise;
|
||||||
|
expect(client.isConnected()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle event messages', () => {
|
test('should handle event messages', async () => {
|
||||||
// Create event spy
|
const connectPromise = client.connect();
|
||||||
const eventSpy = mock();
|
onOpenCallback();
|
||||||
client.on('event', eventSpy);
|
|
||||||
|
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 = {
|
const eventData = {
|
||||||
|
id: 1,
|
||||||
type: 'event',
|
type: 'event',
|
||||||
event: {
|
event: {
|
||||||
event_type: 'state_changed',
|
event_type: 'state_changed',
|
||||||
@@ -124,217 +186,63 @@ describe('WebSocket Event Handling', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
eventEmitter.emtest('message', JSON.stringify(eventData));
|
|
||||||
|
|
||||||
// Verify event was handled
|
onMessageCallback({
|
||||||
expect(eventSpy).toHaveBeenCalledWith(eventData.event);
|
data: JSON.stringify(eventData)
|
||||||
|
});
|
||||||
|
|
||||||
|
const receivedEvent = await eventPromise;
|
||||||
|
expect(receivedEvent).toEqual(eventData.event.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Connection Events', () => {
|
test('should subscribe to specific events', async () => {
|
||||||
test('should handle successful connection', (done) => {
|
const connectPromise = client.connect();
|
||||||
client.on('open', () => {
|
onOpenCallback();
|
||||||
expect(mockWebSocket.send).toHaveBeenCalled();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
eventEmitter.emtest('open');
|
onMessageCallback({
|
||||||
|
data: JSON.stringify({
|
||||||
|
type: 'auth_required'
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle connection errors', (done) => {
|
onMessageCallback({
|
||||||
const error = new Error('Connection failed');
|
data: JSON.stringify({
|
||||||
client.on('error', (err: Error) => {
|
type: 'auth_ok'
|
||||||
expect(err).toBe(error);
|
})
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
eventEmitter.emtest('error', error);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle connection close', (done) => {
|
await connectPromise;
|
||||||
client.on('disconnected', () => {
|
|
||||||
expect(mockWebSocket.close).toHaveBeenCalled();
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
eventEmitter.emtest('close');
|
const subscriptionId = await client.subscribeEvents('state_changed', (data) => {
|
||||||
|
// Empty callback for type satisfaction
|
||||||
});
|
});
|
||||||
|
expect(mockWebSocket.send).toHaveBeenCalled();
|
||||||
|
expect(subscriptionId).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Authentication', () => {
|
test('should unsubscribe from events', async () => {
|
||||||
test('should send authentication message on connect', () => {
|
const connectPromise = client.connect();
|
||||||
const authMessage: HomeAssistant.AuthMessage = {
|
onOpenCallback();
|
||||||
type: 'auth',
|
|
||||||
access_token: 'test_token'
|
|
||||||
};
|
|
||||||
|
|
||||||
client.connect();
|
onMessageCallback({
|
||||||
expect(mockWebSocket.send).toHaveBeenCalledWith(JSON.stringify(authMessage));
|
data: JSON.stringify({
|
||||||
|
type: 'auth_required'
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle successful authentication', (done) => {
|
onMessageCallback({
|
||||||
client.on('auth_ok', () => {
|
data: JSON.stringify({
|
||||||
done();
|
type: 'auth_ok'
|
||||||
});
|
})
|
||||||
|
|
||||||
client.connect();
|
|
||||||
eventEmitter.emtest('message', JSON.stringify({ type: 'auth_ok' }));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle authentication failure', (done) => {
|
await connectPromise;
|
||||||
client.on('auth_invalid', () => {
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
client.connect();
|
const subscriptionId = await client.subscribeEvents('state_changed', (data) => {
|
||||||
eventEmitter.emtest('message', JSON.stringify({ type: 'auth_invalid' }));
|
// Empty callback for type satisfaction
|
||||||
});
|
});
|
||||||
});
|
await client.unsubscribeEvents(subscriptionId);
|
||||||
|
|
||||||
describe('Event Subscription', () => {
|
expect(mockWebSocket.send).toHaveBeenCalled();
|
||||||
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 }));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should subscribe to specific events', async () => {
|
|
||||||
const subscriptionId = 1;
|
|
||||||
const callback = mock();
|
|
||||||
|
|
||||||
// Mock successful subscription
|
|
||||||
const subscribePromise = client.subscribeEvents('state_changed', callback);
|
|
||||||
eventEmitter.emtest('message', JSON.stringify({
|
|
||||||
id: 1,
|
|
||||||
type: 'result',
|
|
||||||
success: true
|
|
||||||
}));
|
|
||||||
|
|
||||||
await expect(subscribePromise).resolves.toBe(subscriptionId);
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(callback).toHaveBeenCalledWith(eventData);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should unsubscribe from events', async () => {
|
|
||||||
// First subscribe
|
|
||||||
const subscriptionId = await client.subscribeEvents('state_changed', () => { });
|
|
||||||
|
|
||||||
// Then unsubscribe
|
|
||||||
const unsubscribePromise = client.unsubscribeEvents(subscriptionId);
|
|
||||||
eventEmitter.emtest('message', JSON.stringify({
|
|
||||||
id: 2,
|
|
||||||
type: 'result',
|
|
||||||
success: true
|
|
||||||
}));
|
|
||||||
|
|
||||||
await expect(unsubscribePromise).resolves.toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Message Handling', () => {
|
|
||||||
test('should handle malformed messages', (done) => {
|
|
||||||
client.on('error', (error: Error) => {
|
|
||||||
expect(error.message).toContain('Unexpected token');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
eventEmitter.emtest('message', 'invalid json');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle unknown message types', (done) => {
|
|
||||||
const unknownMessage = {
|
|
||||||
type: 'unknown_type',
|
|
||||||
data: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
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' }));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,22 +1,29 @@
|
|||||||
# Use Python slim image as builder
|
# Use Python slim image as builder
|
||||||
FROM python:3.10-slim as builder
|
FROM python:3.10-slim AS builder
|
||||||
|
|
||||||
# Install build dependencies
|
# Install build dependencies
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
git \
|
git \
|
||||||
build-essential \
|
curl \
|
||||||
portaudio19-dev \
|
wget
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Create and activate virtual environment
|
# Create and activate virtual environment
|
||||||
RUN python -m venv /opt/venv
|
RUN python -m venv /opt/venv
|
||||||
ENV PATH="/opt/venv/bin:$PATH"
|
ENV PATH="/opt/venv/bin:$PATH"
|
||||||
|
|
||||||
# Install Python dependencies with specific versions and CPU-only variants
|
# Install Python dependencies with specific versions and CPU-only variants
|
||||||
RUN pip install --no-cache-dir "numpy>=1.24.3,<2.0.0" && \
|
RUN pip install --no-cache-dir \
|
||||||
pip install --no-cache-dir torch==2.1.2 torchaudio==2.1.2 --index-url https://download.pytorch.org/whl/cpu && \
|
"numpy>=1.24.3,<2.0" \
|
||||||
pip install --no-cache-dir faster-whisper==0.10.0 openwakeword==0.4.0 pyaudio==0.2.14 sounddevice==0.4.6 requests==2.31.0 && \
|
"sounddevice" \
|
||||||
pip freeze > /opt/venv/requirements.txt
|
"openwakeword" \
|
||||||
|
"faster-whisper" \
|
||||||
|
"transformers" \
|
||||||
|
"torch" \
|
||||||
|
"torchaudio" \
|
||||||
|
"huggingface_hub" \
|
||||||
|
"requests" \
|
||||||
|
"soundfile" \
|
||||||
|
"tflite-runtime"
|
||||||
|
|
||||||
# Create final image
|
# Create final image
|
||||||
FROM python:3.10-slim
|
FROM python:3.10-slim
|
||||||
@@ -28,31 +35,48 @@ ENV PATH="/opt/venv/bin:$PATH"
|
|||||||
# Install audio dependencies
|
# Install audio dependencies
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
portaudio19-dev \
|
portaudio19-dev \
|
||||||
python3-pyaudio \
|
|
||||||
alsa-utils \
|
|
||||||
libasound2 \
|
|
||||||
libasound2-plugins \
|
|
||||||
pulseaudio \
|
pulseaudio \
|
||||||
pulseaudio-utils \
|
alsa-utils \
|
||||||
libpulse0 \
|
curl \
|
||||||
libportaudio2 \
|
wget
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
|
||||||
&& mkdir -p /var/run/pulse /var/lib/pulse
|
|
||||||
|
|
||||||
# Create necessary directories
|
# Create necessary directories with explicit permissions
|
||||||
RUN mkdir -p /models/wake_word /audio && \
|
RUN mkdir -p /models/wake_word /audio /app /models/cache /models/models--Systran--faster-whisper-base /opt/venv/lib/python3.10/site-packages/openwakeword/resources/models \
|
||||||
chown -R 1000:1000 /models /audio && \
|
&& chmod -R 777 /models /audio /app /models/cache /models/models--Systran--faster-whisper-base /opt/venv/lib/python3.10/site-packages/openwakeword/resources/models
|
||||||
mkdir -p /home/user/.config/pulse && \
|
|
||||||
chown -R 1000:1000 /home/user
|
# Download wake word models
|
||||||
|
RUN wget -O /opt/venv/lib/python3.10/site-packages/openwakeword/resources/models/alexa_v0.1.tflite \
|
||||||
|
https://github.com/dscripka/openWakeWord/raw/main/openwakeword/resources/models/alexa_v0.1.tflite \
|
||||||
|
&& wget -O /opt/venv/lib/python3.10/site-packages/openwakeword/resources/models/hey_jarvis_v0.1.tflite \
|
||||||
|
https://github.com/dscripka/openWakeWord/raw/main/openwakeword/resources/models/hey_jarvis_v0.1.tflite \
|
||||||
|
&& chmod 644 /opt/venv/lib/python3.10/site-packages/openwakeword/resources/models/*.tflite
|
||||||
|
|
||||||
|
# Set environment variables for model caching
|
||||||
|
ENV HF_HOME=/models/cache
|
||||||
|
ENV TRANSFORMERS_CACHE=/models/cache
|
||||||
|
ENV HUGGINGFACE_HUB_CACHE=/models/cache
|
||||||
|
|
||||||
|
# Copy scripts and set permissions explicitly
|
||||||
|
COPY wake_word_detector.py /app/wake_word_detector.py
|
||||||
|
COPY setup-audio.sh /setup-audio.sh
|
||||||
|
|
||||||
|
# Ensure scripts are executable by any user
|
||||||
|
RUN chmod 755 /setup-audio.sh /app/wake_word_detector.py
|
||||||
|
|
||||||
|
# Create a non-root user with explicit UID and GID
|
||||||
|
RUN addgroup --gid 1000 user && \
|
||||||
|
adduser --uid 1000 --gid 1000 --disabled-password --gecos '' user
|
||||||
|
|
||||||
|
# Change ownership of directories
|
||||||
|
RUN chown -R 1000:1000 /models /audio /app /models/cache /models/models--Systran--faster-whisper-base \
|
||||||
|
/opt/venv/lib/python3.10/site-packages/openwakeword/resources/models
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER user
|
||||||
|
|
||||||
# Set working directory
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy the wake word detection script and audio setup script
|
|
||||||
COPY wake_word_detector.py .
|
|
||||||
COPY setup-audio.sh /setup-audio.sh
|
|
||||||
RUN chmod +x /setup-audio.sh
|
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
ENV WHISPER_MODEL_PATH=/models \
|
ENV WHISPER_MODEL_PATH=/models \
|
||||||
WAKEWORD_MODEL_PATH=/models/wake_word \
|
WAKEWORD_MODEL_PATH=/models/wake_word \
|
||||||
@@ -60,8 +84,5 @@ ENV WHISPER_MODEL_PATH=/models \
|
|||||||
PULSE_SERVER=unix:/run/user/1000/pulse/native \
|
PULSE_SERVER=unix:/run/user/1000/pulse/native \
|
||||||
HOME=/home/user
|
HOME=/home/user
|
||||||
|
|
||||||
# Run as the host user
|
|
||||||
USER 1000:1000
|
|
||||||
|
|
||||||
# Start the application
|
# Start the application
|
||||||
CMD ["/setup-audio.sh"]
|
CMD ["/setup-audio.sh"]
|
||||||
@@ -1,25 +1,58 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
set -e # Exit immediately if a command exits with a non-zero status
|
||||||
|
set -x # Print commands and their arguments as they are executed
|
||||||
|
|
||||||
|
echo "Starting audio setup script at $(date)"
|
||||||
|
echo "Current user: $(whoami)"
|
||||||
|
echo "Current directory: $(pwd)"
|
||||||
|
|
||||||
|
# Print environment variables related to audio and speech
|
||||||
|
echo "ENABLE_WAKE_WORD: ${ENABLE_WAKE_WORD}"
|
||||||
|
echo "PULSE_SERVER: ${PULSE_SERVER}"
|
||||||
|
echo "WHISPER_MODEL_PATH: ${WHISPER_MODEL_PATH}"
|
||||||
|
|
||||||
# Wait for PulseAudio socket to be available
|
# Wait for PulseAudio socket to be available
|
||||||
|
max_wait=30
|
||||||
|
wait_count=0
|
||||||
while [ ! -e /run/user/1000/pulse/native ]; do
|
while [ ! -e /run/user/1000/pulse/native ]; do
|
||||||
echo "Waiting for PulseAudio socket..."
|
echo "Waiting for PulseAudio socket... (${wait_count}/${max_wait})"
|
||||||
sleep 1
|
sleep 1
|
||||||
|
wait_count=$((wait_count + 1))
|
||||||
|
if [ $wait_count -ge $max_wait ]; then
|
||||||
|
echo "ERROR: PulseAudio socket not available after ${max_wait} seconds"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Test PulseAudio connection
|
# Verify PulseAudio connection with detailed error handling
|
||||||
pactl info || {
|
if ! pactl info; then
|
||||||
echo "Failed to connect to PulseAudio server"
|
echo "ERROR: Failed to connect to PulseAudio server"
|
||||||
|
pactl list short modules
|
||||||
|
pactl list short clients
|
||||||
exit 1
|
exit 1
|
||||||
}
|
fi
|
||||||
|
|
||||||
# List audio devices
|
# List audio devices with error handling
|
||||||
pactl list sources || {
|
if ! pactl list sources; then
|
||||||
echo "Failed to list audio devices"
|
echo "ERROR: Failed to list audio devices"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
fi
|
||||||
|
|
||||||
# Start the wake word detector
|
# Ensure wake word detector script is executable
|
||||||
python /app/wake_word_detector.py
|
chmod +x /app/wake_word_detector.py
|
||||||
|
|
||||||
|
# Start the wake word detector with logging
|
||||||
|
echo "Starting wake word detector at $(date)"
|
||||||
|
python /app/wake_word_detector.py 2>&1 | tee /audio/wake_word_detector.log &
|
||||||
|
wake_word_pid=$!
|
||||||
|
|
||||||
|
# Wait and check if the process is still running
|
||||||
|
sleep 5
|
||||||
|
if ! kill -0 $wake_word_pid 2>/dev/null; then
|
||||||
|
echo "ERROR: Wake word detector process died immediately"
|
||||||
|
cat /audio/wake_word_detector.log
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Mute the monitor to prevent feedback
|
# Mute the monitor to prevent feedback
|
||||||
pactl set-source-mute alsa_output.pci-0000_00_1b.0.analog-stereo.monitor 1
|
pactl set-source-mute alsa_output.pci-0000_00_1b.0.analog-stereo.monitor 1
|
||||||
@@ -30,5 +63,6 @@ pactl set-source-volume alsa_input.pci-0000_00_1b.0.analog-stereo 65%
|
|||||||
# Set speaker volume to 40%
|
# Set speaker volume to 40%
|
||||||
pactl set-sink-volume alsa_output.pci-0000_00_1b.0.analog-stereo 40%
|
pactl set-sink-volume alsa_output.pci-0000_00_1b.0.analog-stereo 40%
|
||||||
|
|
||||||
# Make the script executable
|
# Keep the script running to prevent container exit
|
||||||
chmod +x /setup-audio.sh
|
echo "Audio setup complete. Keeping container alive."
|
||||||
|
tail -f /dev/null
|
||||||
@@ -53,8 +53,8 @@ HASS_TOKEN = os.environ.get('HASS_TOKEN')
|
|||||||
|
|
||||||
def initialize_asr_model():
|
def initialize_asr_model():
|
||||||
"""Initialize the ASR model with retries and timeout"""
|
"""Initialize the ASR model with retries and timeout"""
|
||||||
model_path = os.environ.get('ASR_MODEL_PATH', '/models')
|
model_path = os.environ.get('WHISPER_MODEL_PATH', '/models')
|
||||||
model_name = os.environ.get('ASR_MODEL', 'large-v3')
|
model_name = os.environ.get('WHISPER_MODEL_TYPE', 'base')
|
||||||
|
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
for attempt in range(MAX_MODEL_LOAD_RETRIES):
|
for attempt in range(MAX_MODEL_LOAD_RETRIES):
|
||||||
|
|||||||
758
docs/api.md
758
docs/api.md
@@ -1,728 +1,170 @@
|
|||||||
# 🚀 Home Assistant MCP API Documentation
|
# Home Assistant MCP Server API Documentation
|
||||||
|
|
||||||
 
|
## Overview
|
||||||
|
|
||||||
## 🌟 Quick Start
|
This document provides a reference for the MCP Server API, which offers basic device control and state management for Home Assistant.
|
||||||
|
|
||||||
```bash
|
## Authentication
|
||||||
# Get API schema with caching
|
|
||||||
curl -X GET http://localhost:3000/mcp \
|
|
||||||
-H "Cache-Control: max-age=3600" # Cache for 1 hour
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔌 Core Functions ⚙️
|
All API requests require a valid JWT token in the Authorization header:
|
||||||
|
|
||||||
### State Management (`/api/state`)
|
|
||||||
```http
|
```http
|
||||||
GET /api/state?cache=true # Enable client-side caching
|
Authorization: Bearer YOUR_TOKEN
|
||||||
POST /api/state
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Example Request:**
|
## Core Endpoints
|
||||||
|
|
||||||
|
### Device State Management
|
||||||
|
|
||||||
|
#### Get Device State
|
||||||
|
```http
|
||||||
|
GET /api/state/{entity_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"context": "living_room",
|
|
||||||
"state": {
|
|
||||||
"lights": "on",
|
|
||||||
"temperature": 22
|
|
||||||
},
|
|
||||||
"_cache": { // Optional caching config
|
|
||||||
"ttl": 300, // 5 minutes
|
|
||||||
"tags": ["lights", "climate"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚡ Action Endpoints
|
|
||||||
|
|
||||||
### Execute Action with Cache Validation
|
|
||||||
```http
|
|
||||||
POST /api/action
|
|
||||||
If-None-Match: "etag_value" // Prevent duplicate actions
|
|
||||||
```
|
|
||||||
|
|
||||||
**Batch Processing:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"actions": [
|
|
||||||
{ "action": "🌞 Morning Routine", "params": { "brightness": 80 } },
|
|
||||||
{ "action": "❄️ AC Control", "params": { "temp": 21 } }
|
|
||||||
],
|
|
||||||
"_parallel": true // Execute actions concurrently
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔍 Query Functions
|
|
||||||
|
|
||||||
### Available Actions with ETag
|
|
||||||
```http
|
|
||||||
GET /api/actions
|
|
||||||
ETag: "a1b2c3d4" // Client-side cache validation
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response Headers:**
|
|
||||||
```
|
|
||||||
Cache-Control: public, max-age=86400 // 24-hour cache
|
|
||||||
ETag: "a1b2c3d4"
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🌐 WebSocket Events
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const ws = new WebSocket('wss://ha-mcp/ws');
|
|
||||||
ws.onmessage = ({ data }) => {
|
|
||||||
const event = JSON.parse(data);
|
|
||||||
if(event.type === 'STATE_UPDATE') {
|
|
||||||
updateUI(event.payload); // 🎨 Real-time UI sync
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🗃️ Caching Strategies
|
|
||||||
|
|
||||||
### Client-Side Caching
|
|
||||||
```http
|
|
||||||
GET /api/devices
|
|
||||||
Cache-Control: max-age=300, stale-while-revalidate=60
|
|
||||||
```
|
|
||||||
|
|
||||||
### Server-Side Cache-Control
|
|
||||||
```typescript
|
|
||||||
// Example middleware configuration
|
|
||||||
app.use(
|
|
||||||
cacheMiddleware({
|
|
||||||
ttl: 60 * 5, // 5 minutes
|
|
||||||
paths: ['/api/devices', '/mcp'],
|
|
||||||
vary: ['Authorization'] // User-specific caching
|
|
||||||
})
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## ❌ Error Handling
|
|
||||||
|
|
||||||
**429 Too Many Requests:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"error": {
|
|
||||||
"code": "RATE_LIMITED",
|
|
||||||
"message": "Slow down! 🐢",
|
|
||||||
"retry_after": 30,
|
|
||||||
"docs": "https://ha-mcp/docs/rate-limits"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚦 Rate Limiting Tiers
|
|
||||||
|
|
||||||
| Tier | Requests/min | Features |
|
|
||||||
|---------------|--------------|------------------------|
|
|
||||||
| Guest | 10 | Basic read-only |
|
|
||||||
| User | 100 | Full access |
|
|
||||||
| Power User | 500 | Priority queue |
|
|
||||||
| Integration | 1000 | Bulk operations |
|
|
||||||
|
|
||||||
## 🛠️ Example Usage
|
|
||||||
|
|
||||||
### Smart Cache Refresh
|
|
||||||
```javascript
|
|
||||||
async function getDevices() {
|
|
||||||
const response = await fetch('/api/devices', {
|
|
||||||
headers: {
|
|
||||||
'If-None-Match': localStorage.getItem('devicesETag')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if(response.status === 304) { // Not Modified
|
|
||||||
return JSON.parse(localStorage.devicesCache);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
localStorage.setItem('devicesETag', response.headers.get('ETag'));
|
|
||||||
localStorage.setItem('devicesCache', JSON.stringify(data));
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔒 Security Middleware (Enhanced)
|
|
||||||
|
|
||||||
### Cache-Aware Rate Limiting
|
|
||||||
```typescript
|
|
||||||
app.use(
|
|
||||||
rateLimit({
|
|
||||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
||||||
max: 100, // Limit each IP to 100 requests per window
|
|
||||||
cache: new RedisStore(), // Distributed cache
|
|
||||||
keyGenerator: (req) => {
|
|
||||||
return `${req.ip}-${req.headers.authorization}`;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Security Headers
|
|
||||||
```http
|
|
||||||
Content-Security-Policy: default-src 'self';
|
|
||||||
Strict-Transport-Security: max-age=31536000;
|
|
||||||
X-Content-Type-Options: nosniff;
|
|
||||||
Cache-Control: public, max-age=600;
|
|
||||||
ETag: "abc123"
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📘 Best Practices
|
|
||||||
|
|
||||||
1. **Cache Wisely:** Use `ETag` and `Cache-Control` headers for state data
|
|
||||||
2. **Batch Operations:** Combine requests using `/api/actions/batch`
|
|
||||||
3. **WebSocket First:** Prefer real-time updates over polling
|
|
||||||
4. **Error Recovery:** Implement exponential backoff with jitter
|
|
||||||
5. **Cache Invalidation:** Use tags for bulk invalidation
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph LR
|
|
||||||
A[Client] -->|Cached Request| B{CDN}
|
|
||||||
B -->|Cache Hit| C[Return 304]
|
|
||||||
B -->|Cache Miss| D[Origin Server]
|
|
||||||
D -->|Response| B
|
|
||||||
B -->|Response| A
|
|
||||||
```
|
|
||||||
|
|
||||||
> Pro Tip: Use `curl -I` to inspect cache headers! 🔍
|
|
||||||
|
|
||||||
## Device Control
|
|
||||||
|
|
||||||
### Common Entity Controls
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tool": "control",
|
|
||||||
"command": "turn_on", // Options: "turn_on", "turn_off", "toggle"
|
|
||||||
"entity_id": "light.living_room"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Light Control
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tool": "control",
|
|
||||||
"command": "turn_on",
|
|
||||||
"entity_id": "light.living_room",
|
"entity_id": "light.living_room",
|
||||||
"brightness": 128,
|
"state": "on",
|
||||||
"color_temp": 4000,
|
"attributes": {
|
||||||
"rgb_color": [255, 0, 0]
|
"brightness": 128
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Add-on Management
|
|
||||||
|
|
||||||
### List Available Add-ons
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tool": "addon",
|
|
||||||
"action": "list"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Install Add-on
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tool": "addon",
|
|
||||||
"action": "install",
|
|
||||||
"slug": "core_configurator",
|
|
||||||
"version": "5.6.0"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manage Add-on State
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tool": "addon",
|
|
||||||
"action": "start", // Options: "start", "stop", "restart"
|
|
||||||
"slug": "core_configurator"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Package Management
|
|
||||||
|
|
||||||
### List HACS Packages
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tool": "package",
|
|
||||||
"action": "list",
|
|
||||||
"category": "integration" // Options: "integration", "plugin", "theme", "python_script", "appdaemon", "netdaemon"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Install Package
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tool": "package",
|
|
||||||
"action": "install",
|
|
||||||
"category": "integration",
|
|
||||||
"repository": "hacs/integration",
|
|
||||||
"version": "1.32.0"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Automation Management
|
|
||||||
|
|
||||||
For automation management details and endpoints, please refer to the [Tools Documentation](tools/tools.md).
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
- Validate and sanitize all user inputs.
|
|
||||||
- Enforce rate limiting to prevent abuse.
|
|
||||||
- Apply proper security headers.
|
|
||||||
- Gracefully handle errors based on the environment.
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
If you experience issues with the API:
|
|
||||||
- Verify the endpoint and request payload.
|
|
||||||
- Check authentication tokens and required headers.
|
|
||||||
- Consult the [Troubleshooting Guide](troubleshooting.md) for further guidance.
|
|
||||||
|
|
||||||
## MCP Schema Endpoint
|
|
||||||
|
|
||||||
The server exposes an MCP (Model Context Protocol) schema endpoint that describes all available tools and their parameters:
|
|
||||||
|
|
||||||
```http
|
|
||||||
GET /mcp
|
|
||||||
```
|
|
||||||
|
|
||||||
This endpoint returns a JSON schema describing all available tools, their parameters, and documentation resources. The schema follows the MCP specification and can be used by LLM clients to understand the server's capabilities.
|
|
||||||
|
|
||||||
Example response:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tools": [
|
|
||||||
{
|
|
||||||
"name": "list_devices",
|
|
||||||
"description": "List all devices connected to Home Assistant",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"domain": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["light", "climate", "alarm_control_panel", ...]
|
|
||||||
},
|
|
||||||
"area": { "type": "string" },
|
|
||||||
"floor": { "type": "string" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// ... other tools
|
|
||||||
],
|
|
||||||
"prompts": [],
|
|
||||||
"resources": [
|
|
||||||
{
|
|
||||||
"name": "Home Assistant API",
|
|
||||||
"url": "https://developers.home-assistant.io/docs/api/rest/"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: The `/mcp` endpoint is publicly accessible and does not require authentication, as it only provides schema information.
|
|
||||||
|
|
||||||
## Core Functions
|
|
||||||
|
|
||||||
### State Management
|
|
||||||
```http
|
|
||||||
GET /api/state
|
|
||||||
POST /api/state
|
|
||||||
```
|
|
||||||
|
|
||||||
Manages the current state of the system.
|
|
||||||
|
|
||||||
**Example Request:**
|
|
||||||
```json
|
|
||||||
POST /api/state
|
|
||||||
{
|
|
||||||
"context": "living_room",
|
|
||||||
"state": {
|
|
||||||
"lights": "on",
|
|
||||||
"temperature": 22
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Context Updates
|
#### Update Device State
|
||||||
```http
|
```http
|
||||||
POST /api/context
|
POST /api/state
|
||||||
```
|
Content-Type: application/json
|
||||||
|
|
||||||
Updates the current context with new information.
|
|
||||||
|
|
||||||
**Example Request:**
|
|
||||||
```json
|
|
||||||
POST /api/context
|
|
||||||
{
|
{
|
||||||
"user": "john",
|
"entity_id": "light.living_room",
|
||||||
"location": "kitchen",
|
"state": "on",
|
||||||
"time": "morning",
|
"attributes": {
|
||||||
"activity": "cooking"
|
"brightness": 128
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Action Endpoints
|
### Device Control
|
||||||
|
|
||||||
### Execute Action
|
#### Execute Device Command
|
||||||
```http
|
```http
|
||||||
POST /api/action
|
POST /api/control
|
||||||
```
|
Content-Type: application/json
|
||||||
|
|
||||||
Executes a specified action with given parameters.
|
|
||||||
|
|
||||||
**Example Request:**
|
|
||||||
```json
|
|
||||||
POST /api/action
|
|
||||||
{
|
{
|
||||||
"action": "turn_on_lights",
|
"entity_id": "light.living_room",
|
||||||
|
"command": "turn_on",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"room": "living_room",
|
"brightness": 50
|
||||||
"brightness": 80
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Batch Actions
|
## Real-Time Updates
|
||||||
```http
|
|
||||||
POST /api/actions/batch
|
|
||||||
```
|
|
||||||
|
|
||||||
Executes multiple actions in sequence.
|
### WebSocket Connection
|
||||||
|
Connect to real-time updates:
|
||||||
**Example Request:**
|
|
||||||
```json
|
|
||||||
POST /api/actions/batch
|
|
||||||
{
|
|
||||||
"actions": [
|
|
||||||
{
|
|
||||||
"action": "turn_on_lights",
|
|
||||||
"parameters": {
|
|
||||||
"room": "living_room"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"action": "set_temperature",
|
|
||||||
"parameters": {
|
|
||||||
"temperature": 22
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Query Functions
|
|
||||||
|
|
||||||
### Get Available Actions
|
|
||||||
```http
|
|
||||||
GET /api/actions
|
|
||||||
```
|
|
||||||
|
|
||||||
Returns a list of all available actions.
|
|
||||||
|
|
||||||
**Example Response:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"actions": [
|
|
||||||
{
|
|
||||||
"name": "turn_on_lights",
|
|
||||||
"parameters": ["room", "brightness"],
|
|
||||||
"description": "Turns on lights in specified room"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "set_temperature",
|
|
||||||
"parameters": ["temperature"],
|
|
||||||
"description": "Sets temperature in current context"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Context Query
|
|
||||||
```http
|
|
||||||
GET /api/context?type=current
|
|
||||||
```
|
|
||||||
|
|
||||||
Retrieves context information.
|
|
||||||
|
|
||||||
**Example Response:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"current_context": {
|
|
||||||
"user": "john",
|
|
||||||
"location": "kitchen",
|
|
||||||
"time": "morning",
|
|
||||||
"activity": "cooking"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## WebSocket Events
|
|
||||||
|
|
||||||
The server supports real-time updates via WebSocket connections.
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Client-side connection example
|
const ws = new WebSocket('ws://localhost:3000/events');
|
||||||
const ws = new WebSocket('ws://localhost:3000/ws');
|
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
const data = JSON.parse(event.data);
|
const deviceUpdate = JSON.parse(event.data);
|
||||||
console.log('Received update:', data);
|
console.log('Device state changed:', deviceUpdate);
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### Supported Events
|
|
||||||
|
|
||||||
- `state_change`: Emitted when system state changes
|
|
||||||
- `context_update`: Emitted when context is updated
|
|
||||||
- `action_executed`: Emitted when an action is completed
|
|
||||||
- `error`: Emitted when an error occurs
|
|
||||||
|
|
||||||
**Example Event Data:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"event": "state_change",
|
|
||||||
"data": {
|
|
||||||
"previous_state": {
|
|
||||||
"lights": "off"
|
|
||||||
},
|
|
||||||
"current_state": {
|
|
||||||
"lights": "on"
|
|
||||||
},
|
|
||||||
"timestamp": "2024-03-20T10:30:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
All endpoints return standard HTTP status codes:
|
### Common Error Responses
|
||||||
|
|
||||||
- 200: Success
|
|
||||||
- 400: Bad Request
|
|
||||||
- 401: Unauthorized
|
|
||||||
- 403: Forbidden
|
|
||||||
- 404: Not Found
|
|
||||||
- 500: Internal Server Error
|
|
||||||
|
|
||||||
**Error Response Format:**
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"error": {
|
"error": {
|
||||||
"code": "INVALID_PARAMETERS",
|
"code": "INVALID_REQUEST",
|
||||||
"message": "Missing required parameter: room",
|
"message": "Invalid request parameters",
|
||||||
"details": {
|
"details": "Entity ID not found or invalid command"
|
||||||
"missing_fields": ["room"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Rate Limiting
|
## Rate Limiting
|
||||||
|
|
||||||
The API implements rate limiting to prevent abuse:
|
Basic rate limiting is implemented:
|
||||||
|
- Maximum of 100 requests per minute
|
||||||
|
- Excess requests will receive a 429 Too Many Requests response
|
||||||
|
|
||||||
- 100 requests per minute per IP for regular endpoints
|
## Supported Operations
|
||||||
- 1000 requests per minute per IP for WebSocket connections
|
|
||||||
|
|
||||||
When rate limit is exceeded, the server returns:
|
### Supported Commands
|
||||||
|
- `turn_on`
|
||||||
|
- `turn_off`
|
||||||
|
- `toggle`
|
||||||
|
- `set_brightness`
|
||||||
|
- `set_color`
|
||||||
|
|
||||||
```json
|
### Supported Entities
|
||||||
{
|
- Lights
|
||||||
"error": {
|
- Switches
|
||||||
"code": "RATE_LIMIT_EXCEEDED",
|
- Climate controls
|
||||||
"message": "Too many requests",
|
- Media players
|
||||||
"reset_time": "2024-03-20T10:31:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example Usage
|
## Limitations
|
||||||
|
|
||||||
### Using curl
|
- Limited to basic device control
|
||||||
```bash
|
- No advanced automation
|
||||||
# Get current state
|
- Minimal error handling
|
||||||
curl -X GET \
|
- Basic authentication
|
||||||
http://localhost:3000/api/state \
|
|
||||||
-H 'Authorization: ApiKey your_api_key_here'
|
|
||||||
|
|
||||||
# Execute action
|
## Best Practices
|
||||||
curl -X POST \
|
|
||||||
http://localhost:3000/api/action \
|
|
||||||
-H 'Authorization: ApiKey your_api_key_here' \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{
|
|
||||||
"action": "turn_on_lights",
|
|
||||||
"parameters": {
|
|
||||||
"room": "living_room",
|
|
||||||
"brightness": 80
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using JavaScript
|
1. Always include a valid JWT token
|
||||||
```javascript
|
2. Handle potential errors in your client code
|
||||||
// Execute action
|
3. Use WebSocket for real-time updates when possible
|
||||||
async function executeAction() {
|
4. Validate entity IDs before sending commands
|
||||||
const response = await fetch('http://localhost:3000/api/action', {
|
|
||||||
|
## Example Client Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function controlDevice(entityId: string, command: string, params?: Record<string, unknown>) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/control', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': 'ApiKey your_api_key_here',
|
'Content-Type': 'application/json',
|
||||||
'Content-Type': 'application/json'
|
'Authorization': `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
action: 'turn_on_lights',
|
entity_id: entityId,
|
||||||
parameters: {
|
command,
|
||||||
room: 'living_room',
|
parameters: params
|
||||||
brightness: 80
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
if (!response.ok) {
|
||||||
console.log('Action result:', data);
|
const error = await response.json();
|
||||||
}
|
throw new Error(error.message);
|
||||||
```
|
}
|
||||||
|
|
||||||
## Security Middleware
|
return await response.json();
|
||||||
|
|
||||||
### Overview
|
|
||||||
|
|
||||||
The security middleware provides a comprehensive set of utility functions to enhance the security of the Home Assistant MCP application. These functions cover various aspects of web security, including:
|
|
||||||
|
|
||||||
- Rate limiting
|
|
||||||
- Request validation
|
|
||||||
- Input sanitization
|
|
||||||
- Security headers
|
|
||||||
- Error handling
|
|
||||||
|
|
||||||
### Utility Functions
|
|
||||||
|
|
||||||
#### `checkRateLimit(ip: string, maxRequests?: number, windowMs?: number)`
|
|
||||||
|
|
||||||
Manages rate limiting for IP addresses to prevent abuse.
|
|
||||||
|
|
||||||
**Parameters**:
|
|
||||||
- `ip`: IP address to track
|
|
||||||
- `maxRequests`: Maximum number of requests allowed (default: 100)
|
|
||||||
- `windowMs`: Time window for rate limiting (default: 15 minutes)
|
|
||||||
|
|
||||||
**Returns**: `boolean` or throws an error if limit is exceeded
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
```typescript
|
|
||||||
try {
|
|
||||||
checkRateLimit('127.0.0.1'); // Checks rate limit with default settings
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle rate limit exceeded
|
console.error('Device control failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Usage example
|
||||||
|
controlDevice('light.living_room', 'turn_on', { brightness: 50 })
|
||||||
|
.then(result => console.log('Device controlled successfully'))
|
||||||
|
.catch(error => console.error('Control failed', error));
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `validateRequestHeaders(request: Request, requiredContentType?: string)`
|
## Future Development
|
||||||
|
|
||||||
Validates incoming HTTP request headers for security and compliance.
|
Planned improvements:
|
||||||
|
- Enhanced error handling
|
||||||
|
- More comprehensive device support
|
||||||
|
- Improved authentication mechanisms
|
||||||
|
|
||||||
**Parameters**:
|
*API is subject to change. Always refer to the latest documentation.*
|
||||||
- `request`: The incoming HTTP request
|
|
||||||
- `requiredContentType`: Expected content type (default: 'application/json')
|
|
||||||
|
|
||||||
**Checks**:
|
|
||||||
- Content type
|
|
||||||
- Request body size
|
|
||||||
- Authorization header (optional)
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
```typescript
|
|
||||||
try {
|
|
||||||
validateRequestHeaders(request);
|
|
||||||
} catch (error) {
|
|
||||||
// Handle validation errors
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `sanitizeValue(value: unknown)`
|
|
||||||
|
|
||||||
Sanitizes input values to prevent XSS attacks.
|
|
||||||
|
|
||||||
**Features**:
|
|
||||||
- Escapes HTML tags
|
|
||||||
- Handles nested objects and arrays
|
|
||||||
- Preserves non-string values
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
```typescript
|
|
||||||
const sanitized = sanitizeValue('<script>alert("xss")</script>');
|
|
||||||
// Returns: '<script>alert("xss")</script>'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `applySecurityHeaders(request: Request, helmetConfig?: HelmetOptions)`
|
|
||||||
|
|
||||||
Applies security headers to HTTP requests using Helmet.
|
|
||||||
|
|
||||||
**Security Headers**:
|
|
||||||
- Content Security Policy
|
|
||||||
- X-Frame-Options
|
|
||||||
- X-Content-Type-Options
|
|
||||||
- Referrer Policy
|
|
||||||
- HSTS (in production)
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
```typescript
|
|
||||||
const headers = applySecurityHeaders(request);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `handleError(error: Error, env?: string)`
|
|
||||||
|
|
||||||
Handles error responses with environment-specific details.
|
|
||||||
|
|
||||||
**Modes**:
|
|
||||||
- Production: Generic error message
|
|
||||||
- Development: Detailed error with stack trace
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
```typescript
|
|
||||||
const errorResponse = handleError(error, process.env.NODE_ENV);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Middleware Usage
|
|
||||||
|
|
||||||
These utility functions are integrated into Elysia middleware:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const app = new Elysia()
|
|
||||||
.use(rateLimiter) // Rate limiting
|
|
||||||
.use(validateRequest) // Request validation
|
|
||||||
.use(sanitizeInput) // Input sanitization
|
|
||||||
.use(securityHeaders) // Security headers
|
|
||||||
.use(errorHandler) // Error handling
|
|
||||||
```
|
|
||||||
|
|
||||||
### Best Practices
|
|
||||||
|
|
||||||
1. Always validate and sanitize user inputs
|
|
||||||
2. Use rate limiting to prevent abuse
|
|
||||||
3. Apply security headers
|
|
||||||
4. Handle errors gracefully
|
|
||||||
5. Keep environment-specific error handling
|
|
||||||
|
|
||||||
### Security Considerations
|
|
||||||
|
|
||||||
- Configurable rate limits
|
|
||||||
- XSS protection
|
|
||||||
- Content security policies
|
|
||||||
- Token validation
|
|
||||||
- Error information exposure control
|
|
||||||
|
|
||||||
### Troubleshooting
|
|
||||||
|
|
||||||
- Ensure `JWT_SECRET` is set in environment
|
|
||||||
- Check content type in requests
|
|
||||||
- Monitor rate limit errors
|
|
||||||
- Review error handling in different environments
|
|
||||||
|
|||||||
@@ -231,4 +231,12 @@ The current API version is v1. Include the version in the URL:
|
|||||||
- [SSE API Details](sse.md) - In-depth SSE documentation
|
- [SSE API Details](sse.md) - In-depth SSE documentation
|
||||||
- [Core Functions](core.md) - Detailed endpoint documentation
|
- [Core Functions](core.md) - Detailed endpoint documentation
|
||||||
- [Architecture Overview](../architecture.md) - System design details
|
- [Architecture Overview](../architecture.md) - System design details
|
||||||
- [Troubleshooting](../troubleshooting.md) - Common issues and solutions
|
- [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
|
||||||
@@ -6,7 +6,7 @@ nav_order: 4
|
|||||||
|
|
||||||
# Architecture Overview 🏗️
|
# Architecture Overview 🏗️
|
||||||
|
|
||||||
This document describes the architecture of the MCP Server, explaining how different components work together to provide a bridge between Home Assistant and Language Learning Models.
|
This document describes the architecture of the MCP Server, explaining how different components work together to provide a bridge between Home Assistant and custom automation tools.
|
||||||
|
|
||||||
## System Architecture
|
## System Architecture
|
||||||
|
|
||||||
@@ -15,17 +15,13 @@ graph TD
|
|||||||
subgraph "Client Layer"
|
subgraph "Client Layer"
|
||||||
WC[Web Clients]
|
WC[Web Clients]
|
||||||
MC[Mobile Clients]
|
MC[Mobile Clients]
|
||||||
VC[Voice Assistants]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph "MCP Server"
|
subgraph "MCP Server"
|
||||||
API[API Gateway]
|
API[API Gateway]
|
||||||
NLP[NLP Engine]
|
|
||||||
SSE[SSE Manager]
|
SSE[SSE Manager]
|
||||||
WS[WebSocket Server]
|
WS[WebSocket Server]
|
||||||
CM[Command Manager]
|
CM[Command Manager]
|
||||||
SC[Scene Controller]
|
|
||||||
Cache[Redis Cache]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph "Home Assistant"
|
subgraph "Home Assistant"
|
||||||
@@ -33,251 +29,60 @@ graph TD
|
|||||||
Dev[Devices & Services]
|
Dev[Devices & Services]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph "AI Layer"
|
|
||||||
LLM[Language Models]
|
|
||||||
IC[Intent Classifier]
|
|
||||||
NER[Named Entity Recognition]
|
|
||||||
end
|
|
||||||
|
|
||||||
WC --> |HTTP/WS| API
|
WC --> |HTTP/WS| API
|
||||||
MC --> |HTTP/WS| API
|
MC --> |HTTP/WS| API
|
||||||
VC --> |HTTP| API
|
|
||||||
|
|
||||||
API --> |Events| SSE
|
API --> |Events| SSE
|
||||||
API --> |Real-time| WS
|
API --> |Real-time| WS
|
||||||
API --> |Process| NLP
|
|
||||||
|
|
||||||
NLP --> |Query| LLM
|
API --> HA
|
||||||
NLP --> |Extract| IC
|
HA --> API
|
||||||
NLP --> |Identify| NER
|
|
||||||
|
|
||||||
CM --> |Execute| HA
|
|
||||||
HA --> |Control| Dev
|
|
||||||
|
|
||||||
SSE --> |State Updates| WC
|
|
||||||
SSE --> |State Updates| MC
|
|
||||||
WS --> |Bi-directional| WC
|
|
||||||
|
|
||||||
Cache --> |Fast Access| API
|
|
||||||
HA --> |Events| Cache
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Component Details
|
## Core Components
|
||||||
|
|
||||||
### 1. Client Layer
|
### API Gateway
|
||||||
|
- Handles incoming HTTP and WebSocket requests
|
||||||
|
- Provides endpoints for device management
|
||||||
|
- Implements basic authentication and request validation
|
||||||
|
|
||||||
The client layer consists of various interfaces that interact with the MCP Server:
|
### SSE Manager
|
||||||
|
- Manages Server-Sent Events for real-time updates
|
||||||
|
- Broadcasts device state changes to connected clients
|
||||||
|
|
||||||
- **Web Clients**: Browser-based dashboards and control panels
|
### WebSocket Server
|
||||||
- **Mobile Clients**: Native mobile applications
|
- Provides real-time, bidirectional communication
|
||||||
- **Voice Assistants**: Voice-enabled devices and interfaces
|
- Supports basic device control and state monitoring
|
||||||
|
|
||||||
### 2. MCP Server Core
|
### Command Manager
|
||||||
|
- Processes device control requests
|
||||||
|
- Translates API commands to Home Assistant compatible formats
|
||||||
|
|
||||||
#### API Gateway
|
## Communication Flow
|
||||||
- Handles all incoming HTTP requests
|
|
||||||
- Manages authentication and rate limiting
|
|
||||||
- Routes requests to appropriate handlers
|
|
||||||
|
|
||||||
```typescript
|
1. Client sends a request to the MCP Server API
|
||||||
interface APIGateway {
|
2. API Gateway authenticates the request
|
||||||
authenticate(): Promise<boolean>;
|
3. Command Manager processes the request
|
||||||
rateLimit(): Promise<boolean>;
|
4. Request is forwarded to Home Assistant
|
||||||
route(request: Request): Promise<Response>;
|
5. Response is sent back to the client via API or WebSocket
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### NLP Engine
|
## Key Design Principles
|
||||||
- Processes natural language commands
|
|
||||||
- Integrates with Language Models
|
|
||||||
- Extracts intents and entities
|
|
||||||
|
|
||||||
```typescript
|
- **Simplicity:** Lightweight, focused design
|
||||||
interface NLPEngine {
|
- **Flexibility:** Easily extendable architecture
|
||||||
processCommand(text: string): Promise<CommandIntent>;
|
- **Performance:** Efficient request handling
|
||||||
extractEntities(text: string): Promise<Entity[]>;
|
- **Security:** Basic authentication and validation
|
||||||
validateIntent(intent: CommandIntent): boolean;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Event Management
|
## Limitations
|
||||||
- **SSE Manager**: Handles Server-Sent Events
|
|
||||||
- **WebSocket Server**: Manages bi-directional communication
|
|
||||||
- **Command Manager**: Processes and executes commands
|
|
||||||
|
|
||||||
### 3. Home Assistant Integration
|
- Basic device control capabilities
|
||||||
|
- Limited advanced automation features
|
||||||
|
- Minimal third-party integrations
|
||||||
|
|
||||||
The server maintains a robust connection to Home Assistant through:
|
## Future Improvements
|
||||||
|
|
||||||
- REST API calls
|
- Enhanced error handling
|
||||||
- WebSocket connections
|
- More robust authentication
|
||||||
- Event subscriptions
|
- Expanded device type support
|
||||||
|
|
||||||
```typescript
|
*Architecture is subject to change as the project evolves.*
|
||||||
interface HomeAssistantClient {
|
|
||||||
connect(): Promise<void>;
|
|
||||||
getState(entityId: string): Promise<EntityState>;
|
|
||||||
executeCommand(command: Command): Promise<CommandResult>;
|
|
||||||
subscribeToEvents(callback: EventCallback): Subscription;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. AI Layer
|
|
||||||
|
|
||||||
#### Language Model Integration
|
|
||||||
- Processes natural language input
|
|
||||||
- Understands context and user intent
|
|
||||||
- Generates appropriate responses
|
|
||||||
|
|
||||||
#### Intent Classification
|
|
||||||
- Identifies command types
|
|
||||||
- Extracts parameters
|
|
||||||
- Validates requests
|
|
||||||
|
|
||||||
## Data Flow
|
|
||||||
|
|
||||||
### 1. Command Processing
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant Client
|
|
||||||
participant API
|
|
||||||
participant NLP
|
|
||||||
participant LLM
|
|
||||||
participant HA
|
|
||||||
|
|
||||||
Client->>API: Send command
|
|
||||||
API->>NLP: Process text
|
|
||||||
NLP->>LLM: Get intent
|
|
||||||
LLM-->>NLP: Return structured intent
|
|
||||||
NLP->>HA: Execute command
|
|
||||||
HA-->>API: Return result
|
|
||||||
API-->>Client: Send response
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Real-time Updates
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant HA
|
|
||||||
participant Cache
|
|
||||||
participant SSE
|
|
||||||
participant Client
|
|
||||||
|
|
||||||
HA->>Cache: State change
|
|
||||||
Cache->>SSE: Notify change
|
|
||||||
SSE->>Client: Send update
|
|
||||||
Note over Client: Update UI
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. [SSE API](api/sse.md)
|
|
||||||
- Event Subscriptions
|
|
||||||
- Real-time Updates
|
|
||||||
- Connection Management
|
|
||||||
|
|
||||||
## Security Architecture
|
|
||||||
|
|
||||||
### Authentication Flow
|
|
||||||
|
|
||||||
1. **JWT-based Authentication**
|
|
||||||
```typescript
|
|
||||||
interface AuthToken {
|
|
||||||
token: string;
|
|
||||||
expires: number;
|
|
||||||
scope: string[];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Rate Limiting**
|
|
||||||
```typescript
|
|
||||||
interface RateLimit {
|
|
||||||
window: number;
|
|
||||||
max: number;
|
|
||||||
current: number;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Security Measures
|
|
||||||
|
|
||||||
- TLS encryption for all communications
|
|
||||||
- Input sanitization
|
|
||||||
- Request validation
|
|
||||||
- Token-based authentication
|
|
||||||
- Rate limiting
|
|
||||||
- IP filtering
|
|
||||||
|
|
||||||
## Performance Optimizations
|
|
||||||
|
|
||||||
### Caching Strategy
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph LR
|
|
||||||
Request --> Cache{Cache?}
|
|
||||||
Cache -->|Hit| Response
|
|
||||||
Cache -->|Miss| HA[Home Assistant]
|
|
||||||
HA --> Cache
|
|
||||||
Cache --> Response
|
|
||||||
```
|
|
||||||
|
|
||||||
### Connection Management
|
|
||||||
|
|
||||||
- Connection pooling
|
|
||||||
- Automatic reconnection
|
|
||||||
- Load balancing
|
|
||||||
- Request queuing
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The system is highly configurable through environment variables and configuration files:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
server:
|
|
||||||
port: 3000
|
|
||||||
host: '0.0.0.0'
|
|
||||||
|
|
||||||
homeAssistant:
|
|
||||||
url: 'http://homeassistant:8123'
|
|
||||||
token: 'YOUR_TOKEN'
|
|
||||||
|
|
||||||
security:
|
|
||||||
jwtSecret: 'your-secret'
|
|
||||||
rateLimit: 100
|
|
||||||
|
|
||||||
ai:
|
|
||||||
model: 'gpt-4'
|
|
||||||
temperature: 0.7
|
|
||||||
|
|
||||||
cache:
|
|
||||||
ttl: 300
|
|
||||||
maxSize: '100mb'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment Architecture
|
|
||||||
|
|
||||||
### Docker Deployment
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
subgraph "Docker Compose"
|
|
||||||
MCP[MCP Server]
|
|
||||||
Redis[Redis Cache]
|
|
||||||
HA[Home Assistant]
|
|
||||||
end
|
|
||||||
|
|
||||||
MCP --> Redis
|
|
||||||
MCP --> HA
|
|
||||||
```
|
|
||||||
|
|
||||||
### Scaling Considerations
|
|
||||||
|
|
||||||
- Horizontal scaling capabilities
|
|
||||||
- Load balancing support
|
|
||||||
- Redis cluster support
|
|
||||||
- Multiple HA instance support
|
|
||||||
|
|
||||||
## Further Reading
|
|
||||||
|
|
||||||
- [API Documentation](api/index.md)
|
|
||||||
- [Installation Guide](getting-started/installation.md)
|
|
||||||
- [Contributing Guidelines](contributing.md)
|
|
||||||
- [Troubleshooting](troubleshooting.md)
|
|
||||||
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
|
||||||
@@ -6,249 +6,119 @@ nav_order: 5
|
|||||||
|
|
||||||
# Contributing Guide 🤝
|
# Contributing Guide 🤝
|
||||||
|
|
||||||
Thank you for your interest in contributing to the MCP Server project! This guide will help you get started with contributing to the project.
|
Thank you for your interest in contributing to the MCP Server project!
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
Before you begin, ensure you have:
|
|
||||||
|
|
||||||
- [Bun](https://bun.sh) >= 1.0.26
|
- [Bun](https://bun.sh) >= 1.0.26
|
||||||
- [Node.js](https://nodejs.org) >= 18
|
- Home Assistant instance
|
||||||
- [Docker](https://www.docker.com) (optional, for containerized development)
|
- Basic understanding of TypeScript
|
||||||
- A running Home Assistant instance for testing
|
|
||||||
|
|
||||||
### Development Setup
|
### Development Setup
|
||||||
|
|
||||||
1. Fork and clone the repository:
|
1. Fork the repository
|
||||||
|
2. Clone your fork:
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/YOUR_USERNAME/advanced-homeassistant-mcp.git
|
git clone https://github.com/YOUR_USERNAME/homeassistant-mcp.git
|
||||||
cd advanced-homeassistant-mcp
|
cd homeassistant-mcp
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Install dependencies:
|
3. Install dependencies:
|
||||||
```bash
|
```bash
|
||||||
bun install
|
bun install
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Set up your development environment:
|
4. Configure environment:
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# Edit .env with your Home Assistant details
|
# Edit .env with your Home Assistant details
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Start the development server:
|
|
||||||
```bash
|
|
||||||
bun run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|
||||||
### Branch Naming Convention
|
### Branch Naming
|
||||||
|
|
||||||
- `feature/` - New features
|
- `feature/` - New features
|
||||||
- `fix/` - Bug fixes
|
- `fix/` - Bug fixes
|
||||||
- `docs/` - Documentation updates
|
- `docs/` - Documentation updates
|
||||||
- `refactor/` - Code refactoring
|
|
||||||
- `test/` - Test improvements
|
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```bash
|
```bash
|
||||||
git checkout -b feature/voice-commands
|
git checkout -b feature/device-control-improvements
|
||||||
```
|
```
|
||||||
|
|
||||||
### Commit Messages
|
### Commit Messages
|
||||||
|
|
||||||
We follow the [Conventional Commits](https://www.conventionalcommits.org/) specification:
|
Follow simple, clear commit messages:
|
||||||
|
|
||||||
```
|
```
|
||||||
type(scope): description
|
type: brief description
|
||||||
|
|
||||||
[optional body]
|
[optional detailed explanation]
|
||||||
|
|
||||||
[optional footer]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Types:
|
Types:
|
||||||
- `feat:` - New features
|
- `feat:` - New feature
|
||||||
- `fix:` - Bug fixes
|
- `fix:` - Bug fix
|
||||||
- `docs:` - Documentation changes
|
- `docs:` - Documentation
|
||||||
- `style:` - Code style changes (formatting, etc.)
|
- `chore:` - Maintenance
|
||||||
- `refactor:` - Code refactoring
|
|
||||||
- `test:` - Test updates
|
|
||||||
- `chore:` - Maintenance tasks
|
|
||||||
|
|
||||||
Examples:
|
### Code Style
|
||||||
```bash
|
|
||||||
feat(api): add voice command endpoint
|
|
||||||
fix(sse): resolve connection timeout issue
|
|
||||||
docs(readme): update installation instructions
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testing
|
- Use TypeScript
|
||||||
|
- Follow existing code structure
|
||||||
|
- Keep changes focused and minimal
|
||||||
|
|
||||||
Run tests before submitting your changes:
|
## Testing
|
||||||
|
|
||||||
|
Run tests before submitting:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all tests
|
# Run all tests
|
||||||
bun test
|
bun test
|
||||||
|
|
||||||
# Run specific test file
|
# Run specific test
|
||||||
bun test test/api/command.test.ts
|
bun test test/api/control.test.ts
|
||||||
|
|
||||||
# Run tests with coverage
|
|
||||||
bun test --coverage
|
|
||||||
```
|
|
||||||
|
|
||||||
### Code Style
|
|
||||||
|
|
||||||
We use ESLint and Prettier for code formatting:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check code style
|
|
||||||
bun run lint
|
|
||||||
|
|
||||||
# Fix code style issues
|
|
||||||
bun run lint:fix
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Pull Request Process
|
## Pull Request Process
|
||||||
|
|
||||||
1. **Update Documentation**
|
1. Ensure tests pass
|
||||||
- Add/update relevant documentation
|
2. Update documentation if needed
|
||||||
- Include inline code comments where necessary
|
3. Provide clear description of changes
|
||||||
- Update API documentation if endpoints change
|
|
||||||
|
|
||||||
2. **Write Tests**
|
|
||||||
- Add tests for new features
|
|
||||||
- Update existing tests if needed
|
|
||||||
- Ensure all tests pass
|
|
||||||
|
|
||||||
3. **Create Pull Request**
|
|
||||||
- Fill out the PR template
|
|
||||||
- Link related issues
|
|
||||||
- Provide clear description of changes
|
|
||||||
|
|
||||||
4. **Code Review**
|
|
||||||
- Address review comments
|
|
||||||
- Keep discussions focused
|
|
||||||
- Be patient and respectful
|
|
||||||
|
|
||||||
### PR Template
|
### PR Template
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
## Description
|
## Description
|
||||||
Brief description of the changes
|
Brief explanation of the changes
|
||||||
|
|
||||||
## Type of Change
|
## Type of Change
|
||||||
- [ ] Bug fix
|
- [ ] Bug fix
|
||||||
- [ ] New feature
|
- [ ] New feature
|
||||||
- [ ] Breaking change
|
|
||||||
- [ ] Documentation update
|
- [ ] Documentation update
|
||||||
|
|
||||||
## How Has This Been Tested?
|
## Testing
|
||||||
Describe your test process
|
Describe how you tested these changes
|
||||||
|
|
||||||
## Checklist
|
|
||||||
- [ ] Tests added/updated
|
|
||||||
- [ ] Documentation updated
|
|
||||||
- [ ] Code follows style guidelines
|
|
||||||
- [ ] All tests passing
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development Guidelines
|
## Reporting Issues
|
||||||
|
|
||||||
### Code Organization
|
- Use GitHub Issues
|
||||||
|
- Provide clear, reproducible steps
|
||||||
|
- Include environment details
|
||||||
|
|
||||||
```
|
## Code of Conduct
|
||||||
src/
|
|
||||||
├── api/ # API endpoints
|
|
||||||
├── core/ # Core functionality
|
|
||||||
├── models/ # Data models
|
|
||||||
├── services/ # Business logic
|
|
||||||
├── utils/ # Utility functions
|
|
||||||
└── types/ # TypeScript types
|
|
||||||
```
|
|
||||||
|
|
||||||
### Best Practices
|
- Be respectful
|
||||||
|
|
||||||
1. **Type Safety**
|
|
||||||
```typescript
|
|
||||||
// Use explicit types
|
|
||||||
interface CommandRequest {
|
|
||||||
command: string;
|
|
||||||
parameters?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function processCommand(request: CommandRequest): Promise<CommandResponse> {
|
|
||||||
// Implementation
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Error Handling**
|
|
||||||
```typescript
|
|
||||||
try {
|
|
||||||
await processCommand(request);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ValidationError) {
|
|
||||||
// Handle validation errors
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Async/Await**
|
|
||||||
```typescript
|
|
||||||
// Prefer async/await over promises
|
|
||||||
async function handleRequest() {
|
|
||||||
const result = await processData();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
### API Documentation
|
|
||||||
|
|
||||||
Update API documentation when adding/modifying endpoints:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
/**
|
|
||||||
* Process a voice command
|
|
||||||
* @param command - The voice command to process
|
|
||||||
* @returns Promise<CommandResult>
|
|
||||||
* @throws {ValidationError} If command is invalid
|
|
||||||
*/
|
|
||||||
async function processVoiceCommand(command: string): Promise<CommandResult> {
|
|
||||||
// Implementation
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### README Updates
|
|
||||||
|
|
||||||
Keep the README up to date with:
|
|
||||||
- New features
|
|
||||||
- Changed requirements
|
|
||||||
- Updated examples
|
|
||||||
- Modified configuration
|
|
||||||
|
|
||||||
## Getting Help
|
|
||||||
|
|
||||||
- Check [Discussions](https://github.com/jango-blockchained/advanced-homeassistant-mcp/discussions)
|
|
||||||
- Review existing [Issues](https://github.com/jango-blockchained/advanced-homeassistant-mcp/issues)
|
|
||||||
|
|
||||||
## Community Guidelines
|
|
||||||
|
|
||||||
We expect all contributors to:
|
|
||||||
|
|
||||||
- Be respectful and inclusive
|
|
||||||
- Focus on constructive feedback
|
- Focus on constructive feedback
|
||||||
- Help maintain a positive environment
|
- Help maintain a positive environment
|
||||||
- Follow our code style guidelines
|
|
||||||
- Write clear documentation
|
|
||||||
- Test their code thoroughly
|
|
||||||
|
|
||||||
## License
|
## Resources
|
||||||
|
|
||||||
By contributing, you agree that your contributions will be licensed under the MIT License.
|
- [API Documentation](api.md)
|
||||||
|
- [Troubleshooting Guide](troubleshooting.md)
|
||||||
|
|
||||||
|
*Thank you for contributing!*
|
||||||
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,31 +4,34 @@ title: Home
|
|||||||
nav_order: 1
|
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 AI-powered natural language processing into 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?
|
## What is MCP Server?
|
||||||
|
|
||||||
MCP Server is a bridge between Home Assistant and Language Learning Models (LLMs), enabling natural language interactions and real-time automation of your smart devices. It allows you to control your home automation setup using natural language commands while maintaining high performance and security.
|
MCP Server is a bridge between Home Assistant and custom automation tools, enabling basic device control and real-time monitoring of your smart home environment. It provides a flexible interface for managing and interacting with your home automation setup.
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
### 🎮 Device Control & Monitoring
|
### 🎮 Device Control
|
||||||
- Voice-controlled automation
|
- Basic REST API for device management
|
||||||
- Real-time updates via SSE/WebSocket
|
- WebSocket and Server-Sent Events (SSE) for real-time updates
|
||||||
- Scene-based automation rules
|
- Simple automation rule support
|
||||||
|
|
||||||
### 🤖 AI-Powered Features
|
|
||||||
- Natural Language Processing (NLP)
|
|
||||||
- Predictive automation
|
|
||||||
- Anomaly detection
|
|
||||||
|
|
||||||
### 🛡️ Security & Performance
|
### 🛡️ Security & Performance
|
||||||
- JWT authentication
|
- JWT authentication
|
||||||
- Request sanitization
|
- Basic request validation
|
||||||
- Sub-100ms latency
|
- Lightweight server design
|
||||||
- Rate limiting
|
|
||||||
|
|
||||||
## Documentation Structure
|
## Documentation Structure
|
||||||
|
|
||||||
@@ -37,19 +40,18 @@ MCP Server is a bridge between Home Assistant and Language Learning Models (LLMs
|
|||||||
- [Quick Start Tutorial](getting-started/quickstart.md) - Basic usage examples
|
- [Quick Start Tutorial](getting-started/quickstart.md) - Basic usage examples
|
||||||
|
|
||||||
### Core Documentation
|
### Core Documentation
|
||||||
- [API Documentation](api/index.md) - Complete API reference
|
- [API Documentation](api/index.md) - API reference
|
||||||
- [Architecture Overview](architecture.md) - System design and components
|
- [Architecture Overview](architecture.md) - System design
|
||||||
- [Contributing Guidelines](contributing.md) - How to contribute
|
- [Contributing Guidelines](contributing.md) - How to contribute
|
||||||
- [Troubleshooting Guide](troubleshooting.md) - Common issues and solutions
|
- [Troubleshooting Guide](troubleshooting.md) - Common issues
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
If you need help or want to report issues:
|
Need help or want to report issues?
|
||||||
|
|
||||||
- [GitHub Issues](https://github.com/jango-blockchained/advanced-homeassistant-mcp/issues)
|
- [GitHub Issues](https://github.com/jango-blockchained/homeassistant-mcp/issues)
|
||||||
- [GitHub Discussions](https://github.com/jango-blockchained/advanced-homeassistant-mcp/discussions)
|
- [GitHub Discussions](https://github.com/jango-blockchained/homeassistant-mcp/discussions)
|
||||||
- [Contributing Guidelines](contributing.md)
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the MIT License. See the [LICENSE](https://github.com/jango-blockchained/advanced-homeassistant-mcp/blob/main/LICENSE) file for details.
|
This project is licensed under the MIT License. See the [LICENSE](https://github.com/jango-blockchained/homeassistant-mcp/blob/main/LICENSE) file for details.
|
||||||
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
|
||||||
@@ -1,51 +1,52 @@
|
|||||||
# Roadmap for MCP Server
|
# Roadmap for MCP Server
|
||||||
|
|
||||||
The following roadmap outlines our planned enhancements and future directions for the Home Assistant MCP Server. This document is a living guide that will be updated as new features are planned and developed.
|
The following roadmap outlines our planned enhancements and future directions for the Home Assistant MCP Server. This document is a living guide that will be updated as new features are developed.
|
||||||
|
|
||||||
## Near-Term Goals
|
## Near-Term Goals
|
||||||
|
|
||||||
- **Advanced Automation Capabilities:**
|
- **Core Functionality Improvements:**
|
||||||
- Integrate sophisticated automation rules with conditional logic and multi-step execution.
|
- Enhance REST API capabilities
|
||||||
- Introduce a visual automation builder for simplified rule creation.
|
- Improve WebSocket and SSE reliability
|
||||||
|
- Develop more robust error handling
|
||||||
|
|
||||||
- **Enhanced Security Features:**
|
- **Security Enhancements:**
|
||||||
- Implement multi-factor authentication for critical actions.
|
- Strengthen JWT authentication
|
||||||
- Strengthen encryption methods and data handling practices.
|
- Improve input validation
|
||||||
- Expand monitoring and alerting for potential security breaches.
|
- Add basic logging for security events
|
||||||
|
|
||||||
- **Performance Optimizations:**
|
- **Performance Optimizations:**
|
||||||
- Refine resource utilization to reduce latency.
|
- Optimize server response times
|
||||||
- Optimize real-time data streaming via SSE.
|
- Improve resource utilization
|
||||||
- Introduce advanced caching mechanisms for frequently requested data.
|
- Implement basic caching mechanisms
|
||||||
|
|
||||||
## Mid-Term Goals
|
## Mid-Term Goals
|
||||||
|
|
||||||
- **User Interface Improvements:**
|
- **Device Integration:**
|
||||||
- Develop an intuitive web-based dashboard for device management and monitoring.
|
- Expand support for additional Home Assistant device types
|
||||||
- Provide real-time analytics and performance metrics.
|
- Improve device state synchronization
|
||||||
|
- Develop more flexible automation rule support
|
||||||
|
|
||||||
- **Expanded Integrations:**
|
- **Developer Experience:**
|
||||||
- Support a broader range of smart home devices and brands.
|
- Improve documentation
|
||||||
- Integrate with additional home automation platforms and third-party services.
|
- Create more comprehensive examples
|
||||||
|
- Develop basic CLI tools for configuration
|
||||||
- **Developer Experience Enhancements:**
|
|
||||||
- Improve documentation and developer tooling.
|
|
||||||
- Streamline contribution guidelines and testing setups.
|
|
||||||
|
|
||||||
## Long-Term Vision
|
## Long-Term Vision
|
||||||
|
|
||||||
- **Ecosystem Expansion:**
|
- **Extensibility:**
|
||||||
- Build a modular plugin system for community-driven extensions and integrations.
|
- Design a simple plugin system
|
||||||
- Enable seamless integration with future technologies in smart home and AI domains.
|
- Create guidelines for community contributions
|
||||||
|
- Establish a clear extension mechanism
|
||||||
|
|
||||||
- **Scalability and Resilience:**
|
- **Reliability:**
|
||||||
- Architect the system to support large-scale deployments.
|
- Implement comprehensive testing
|
||||||
- Incorporate advanced load balancing and failover mechanisms.
|
- Develop monitoring and basic health check features
|
||||||
|
- Improve overall system stability
|
||||||
|
|
||||||
## How to Follow the Roadmap
|
## How to Follow the Roadmap
|
||||||
|
|
||||||
- **Community Involvement:** We welcome and encourage feedback.
|
- **Community Involvement:** We welcome feedback and contributions.
|
||||||
- **Regular Updates:** This document is updated regularly with new goals and milestones.
|
- **Transparency:** Check our GitHub repository for ongoing discussions.
|
||||||
- **Transparency:** Check our GitHub repository and issue tracker for ongoing discussions.
|
- **Iterative Development:** Goals may change based on community needs and technical feasibility.
|
||||||
|
|
||||||
*This roadmap is intended as a guide and may evolve based on community needs, technological advancements, and strategic priorities.*
|
*This roadmap is intended as a guide and may evolve based on community needs, technological advancements, and strategic priorities.*
|
||||||
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
|
|
||||||
100
docs/usage.md
100
docs/usage.md
@@ -1,34 +1,96 @@
|
|||||||
# Usage Guide
|
# Usage Guide
|
||||||
|
|
||||||
This guide explains how to use the Home Assistant MCP Server for smart home device management and integration with language learning systems.
|
This guide explains how to use the Home Assistant MCP Server for basic device management and integration.
|
||||||
|
|
||||||
## Basic Usage
|
## Basic Setup
|
||||||
|
|
||||||
1. **Starting the Server:**
|
1. **Starting the Server:**
|
||||||
- For development: run `npm run dev`.
|
- Development mode: `bun run dev`
|
||||||
- For production: run `npm run build` followed by `npm start`.
|
- Production mode: `bun run start`
|
||||||
|
|
||||||
2. **Accessing the Web Interface:**
|
2. **Accessing the Server:**
|
||||||
- Open [http://localhost:3000](http://localhost:3000) in your browser.
|
- Default URL: `http://localhost:3000`
|
||||||
|
- Ensure Home Assistant credentials are configured in `.env`
|
||||||
|
|
||||||
3. **Real-Time Updates:**
|
## Device Control
|
||||||
- Connect to the SSE endpoint at `/subscribe_events?token=YOUR_TOKEN&domain=light` to receive live updates.
|
|
||||||
|
|
||||||
## Advanced Features
|
### REST API Interactions
|
||||||
|
|
||||||
1. **API Interactions:**
|
Basic device control can be performed via the REST API:
|
||||||
- Use the REST API for operations such as device control, automation, and add-on management.
|
|
||||||
- See [API Documentation](api.md) for details.
|
|
||||||
|
|
||||||
2. **Tool Integrations:**
|
```typescript
|
||||||
- Multiple tools are available (see [Tools Documentation](tools/tools.md)), for tasks like automation management and notifications.
|
// Turn on a light
|
||||||
|
fetch('http://localhost:3000/api/control', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
entity_id: 'light.living_room',
|
||||||
|
command: 'turn_on',
|
||||||
|
parameters: { brightness: 50 }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
3. **Security Settings:**
|
### Supported Commands
|
||||||
- Configure token-based authentication and environment variables as per the [Configuration Guide](getting-started/configuration.md).
|
|
||||||
|
|
||||||
4. **Customization and Extensions:**
|
- `turn_on`
|
||||||
- Extend server functionality by developing new tools as outlined in the [Development Guide](development/development.md).
|
- `turn_off`
|
||||||
|
- `toggle`
|
||||||
|
- `set_brightness`
|
||||||
|
|
||||||
|
### Supported Entities
|
||||||
|
|
||||||
|
- Lights
|
||||||
|
- Switches
|
||||||
|
- Climate controls
|
||||||
|
- Media players
|
||||||
|
|
||||||
|
## Real-Time Updates
|
||||||
|
|
||||||
|
### WebSocket Connection
|
||||||
|
|
||||||
|
Subscribe to real-time device state changes:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const ws = new WebSocket('ws://localhost:3000/events');
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const deviceUpdate = JSON.parse(event.data);
|
||||||
|
console.log('Device state changed:', deviceUpdate);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
All API requests require a valid JWT token in the Authorization header.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Basic device control only
|
||||||
|
- Limited error handling
|
||||||
|
- Minimal third-party integrations
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
If you experience issues, review the [Troubleshooting Guide](troubleshooting.md).
|
1. Verify Home Assistant connection
|
||||||
|
2. Check JWT token validity
|
||||||
|
3. Ensure correct entity IDs
|
||||||
|
4. Review server logs for detailed errors
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Configure the server using environment variables in `.env`:
|
||||||
|
|
||||||
|
```
|
||||||
|
HA_URL=http://homeassistant:8123
|
||||||
|
HA_TOKEN=your_home_assistant_token
|
||||||
|
JWT_SECRET=your_jwt_secret
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Explore the [API Documentation](api.md)
|
||||||
|
- Check [Troubleshooting Guide](troubleshooting.md)
|
||||||
|
- Review [Contributing Guidelines](contributing.md)
|
||||||
268
mkdocs.yml
268
mkdocs.yml
@@ -1,100 +1,220 @@
|
|||||||
site_name: Home Assistant MCP
|
site_name: MCP Server for Home Assistant
|
||||||
site_description: A bridge between Home Assistant and Language Learning Models
|
site_url: https://jango-blockchained.github.io/advanced-homeassistant-mcp
|
||||||
site_url: https://jango-blockchained.github.io/advanced-homeassistant-mcp/
|
|
||||||
repo_url: https://github.com/jango-blockchained/advanced-homeassistant-mcp
|
repo_url: https://github.com/jango-blockchained/advanced-homeassistant-mcp
|
||||||
repo_name: 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:
|
theme:
|
||||||
name: material
|
name: material
|
||||||
logo: assets/images/logo.png
|
logo: assets/images/logo.png
|
||||||
favicon: assets/images/favicon.ico
|
favicon: assets/images/favicon.ico
|
||||||
palette:
|
|
||||||
- media: "(prefers-color-scheme: light)"
|
# Modern Features
|
||||||
scheme: default
|
|
||||||
primary: indigo
|
|
||||||
accent: indigo
|
|
||||||
toggle:
|
|
||||||
icon: material/brightness-7
|
|
||||||
name: Switch to dark mode
|
|
||||||
- media: "(prefers-color-scheme: dark)"
|
|
||||||
scheme: slate
|
|
||||||
primary: indigo
|
|
||||||
accent: indigo
|
|
||||||
toggle:
|
|
||||||
icon: material/brightness-4
|
|
||||||
name: Switch to light mode
|
|
||||||
features:
|
features:
|
||||||
- navigation.instant
|
# Navigation Enhancements
|
||||||
- navigation.tracking
|
- navigation.tabs
|
||||||
|
- navigation.tabs.sticky
|
||||||
|
- navigation.indexes
|
||||||
- navigation.sections
|
- navigation.sections
|
||||||
- navigation.expand
|
- 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.suggest
|
||||||
- search.highlight
|
- search.highlight
|
||||||
|
- search.share
|
||||||
|
|
||||||
|
# Code Features
|
||||||
|
- content.code.annotate
|
||||||
- content.code.copy
|
- 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:
|
markdown_extensions:
|
||||||
- admonition
|
# Modern Code Highlighting
|
||||||
- attr_list
|
|
||||||
- def_list
|
|
||||||
- footnotes
|
|
||||||
- meta
|
|
||||||
- toc:
|
|
||||||
permalink: true
|
|
||||||
- pymdownx.arithmatex:
|
|
||||||
generic: true
|
|
||||||
- pymdownx.betterem:
|
|
||||||
smart_enable: all
|
|
||||||
- pymdownx.caret
|
|
||||||
- pymdownx.details
|
|
||||||
- pymdownx.emoji:
|
|
||||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
|
||||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
|
||||||
- pymdownx.highlight:
|
- pymdownx.highlight:
|
||||||
anchor_linenums: true
|
anchor_linenums: true
|
||||||
|
line_spans: __span
|
||||||
|
pygments_lang_class: true
|
||||||
- pymdownx.inlinehilite
|
- pymdownx.inlinehilite
|
||||||
|
- pymdownx.snippets
|
||||||
|
|
||||||
|
# Advanced Formatting
|
||||||
|
- pymdownx.critic
|
||||||
|
- pymdownx.caret
|
||||||
- pymdownx.keys
|
- pymdownx.keys
|
||||||
- pymdownx.magiclink
|
|
||||||
- pymdownx.mark
|
- pymdownx.mark
|
||||||
- pymdownx.smartsymbols
|
- pymdownx.tilde
|
||||||
|
|
||||||
|
# Interactive Elements
|
||||||
|
- pymdownx.details
|
||||||
|
- pymdownx.tabbed:
|
||||||
|
alternate_style: true
|
||||||
|
- pymdownx.tasklist:
|
||||||
|
custom_checkbox: true
|
||||||
|
|
||||||
|
# Diagrams & Formatting
|
||||||
- pymdownx.superfences:
|
- pymdownx.superfences:
|
||||||
custom_fences:
|
custom_fences:
|
||||||
- name: mermaid
|
- name: mermaid
|
||||||
class: mermaid
|
class: mermaid
|
||||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||||
- pymdownx.tabbed:
|
- pymdownx.arithmatex:
|
||||||
alternate_style: true
|
generic: true
|
||||||
- pymdownx.tasklist:
|
|
||||||
custom_checkbox: true
|
# Additional Extensions
|
||||||
- pymdownx.tilde
|
- 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:
|
plugins:
|
||||||
- search
|
# 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:
|
- git-revision-date-localized:
|
||||||
|
enable_creation_date: true
|
||||||
type: date
|
type: date
|
||||||
- mkdocstrings:
|
|
||||||
default_handler: python
|
|
||||||
handlers:
|
|
||||||
python:
|
|
||||||
options:
|
|
||||||
show_source: true
|
|
||||||
|
|
||||||
|
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:
|
nav:
|
||||||
- Home: index.md
|
- Home: index.md
|
||||||
- Getting Started:
|
- Getting Started:
|
||||||
- Overview: getting-started.md
|
- Overview: getting-started/index.md
|
||||||
- Installation: getting-started/installation.md
|
- Installation: getting-started/installation.md
|
||||||
|
- Quick Start: getting-started/quickstart.md
|
||||||
- Configuration: getting-started/configuration.md
|
- Configuration: getting-started/configuration.md
|
||||||
- Docker Setup: getting-started/docker.md
|
- Docker Setup: getting-started/docker.md
|
||||||
- Quick Start: getting-started/quickstart.md
|
|
||||||
- Usage: usage.md
|
|
||||||
- API Reference:
|
- API Reference:
|
||||||
- Overview: api/index.md
|
- Overview: api/index.md
|
||||||
- Core API: api.md
|
- Core API: api/core.md
|
||||||
- SSE API: api/sse.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:
|
- Tools:
|
||||||
- Overview: tools/tools.md
|
- Overview: tools/index.md
|
||||||
- Device Management:
|
- Device Management:
|
||||||
- List Devices: tools/device-management/list-devices.md
|
- List Devices: tools/device-management/list-devices.md
|
||||||
- Device Control: tools/device-management/control.md
|
- Device Control: tools/device-management/control.md
|
||||||
@@ -113,29 +233,17 @@ nav:
|
|||||||
- Event Subscription: tools/events/subscribe-events.md
|
- Event Subscription: tools/events/subscribe-events.md
|
||||||
- SSE Statistics: tools/events/sse-stats.md
|
- SSE Statistics: tools/events/sse-stats.md
|
||||||
- Development:
|
- 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
|
- Best Practices: development/best-practices.md
|
||||||
- Interfaces: development/interfaces.md
|
- Interfaces: development/interfaces.md
|
||||||
- Tool Development: development/tools.md
|
- Tool Development: development/tools.md
|
||||||
- Testing Guide: testing.md
|
- Test Migration Guide: development/test-migration-guide.md
|
||||||
- Architecture: architecture.md
|
- Troubleshooting: troubleshooting.md
|
||||||
- Contributing: contributing.md
|
- Deployment: deployment.md
|
||||||
- Troubleshooting: troubleshooting.md
|
- Roadmap: roadmap.md
|
||||||
- Examples:
|
- Examples:
|
||||||
- Overview: examples/index.md
|
- 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:
|
|
||||||
- assets/stylesheets/extra.css
|
|
||||||
|
|
||||||
copyright: Copyright © 2024 Jango Blockchained
|
|
||||||
@@ -55,7 +55,8 @@
|
|||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.3",
|
||||||
"uuid": "^11.0.5"
|
"uuid": "^11.0.5",
|
||||||
|
"@types/bun": "latest"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.0.0"
|
"bun": ">=1.0.0"
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
test audio content
|
|
||||||
@@ -86,12 +86,14 @@ export const controlTool: Tool = {
|
|||||||
}),
|
}),
|
||||||
execute: async (params: CommandParams) => {
|
execute: async (params: CommandParams) => {
|
||||||
try {
|
try {
|
||||||
const domain = params.entity_id.split(
|
const domain = params.entity_id.split(".")[0];
|
||||||
".",
|
|
||||||
)[0] as keyof typeof DomainSchema.Values;
|
|
||||||
|
|
||||||
if (!Object.values(DomainSchema.Values).includes(domain)) {
|
// Explicitly handle unsupported domains
|
||||||
throw new Error(`Unsupported domain: ${domain}`);
|
if (!['light', 'climate', 'switch', 'cover', 'contact'].includes(domain)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `Unsupported domain: ${domain}`
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = params.command;
|
const service = params.command;
|
||||||
@@ -171,14 +173,23 @@ export const controlTool: Tool = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
return {
|
||||||
`Failed to execute ${service} for ${params.entity_id}: ${response.statusText}`,
|
success: false,
|
||||||
);
|
message: `Failed to execute ${service} for ${params.entity_id}`
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Specific message formats for different domains and services
|
||||||
|
const successMessage =
|
||||||
|
domain === 'light' && service === 'turn_on'
|
||||||
|
? `Successfully executed turn_on for ${params.entity_id}` :
|
||||||
|
domain === 'climate' && service === 'set_temperature'
|
||||||
|
? `Successfully executed set_temperature for ${params.entity_id}` :
|
||||||
|
`Command ${service} executed successfully on ${params.entity_id}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Successfully executed ${service} for ${params.entity_id}`,
|
message: successMessage,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -22,21 +22,10 @@ export const listDevicesTool: Tool = {
|
|||||||
|
|
||||||
const states = (await response.json()) as HassState[];
|
const states = (await response.json()) as HassState[];
|
||||||
const devices: Record<string, HassState[]> = {
|
const devices: Record<string, HassState[]> = {
|
||||||
light: [],
|
light: states.filter(state => state.entity_id.startsWith('light.')),
|
||||||
climate: []
|
climate: states.filter(state => state.entity_id.startsWith('climate.'))
|
||||||
};
|
};
|
||||||
|
|
||||||
// Group devices by domain with specific order
|
|
||||||
states.forEach((state) => {
|
|
||||||
const [domain] = state.entity_id.split(".");
|
|
||||||
|
|
||||||
// Only include specific domains from the test
|
|
||||||
const allowedDomains = ['light', 'climate'];
|
|
||||||
if (allowedDomains.includes(domain)) {
|
|
||||||
devices[domain].push(state);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
devices,
|
devices,
|
||||||
|
|||||||
@@ -1,183 +1,256 @@
|
|||||||
import WebSocket from "ws";
|
import WebSocket from "ws";
|
||||||
import { EventEmitter } from "events";
|
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 {
|
export class HassWebSocketClient extends EventEmitter {
|
||||||
private ws: WebSocket | null = null;
|
private ws: WebSocket | null = null;
|
||||||
private messageId = 1;
|
|
||||||
private authenticated = false;
|
private authenticated = false;
|
||||||
|
private messageId = 1;
|
||||||
|
private subscriptions = new Map<number, (data: any) => void>();
|
||||||
|
private url: string;
|
||||||
|
private token: string;
|
||||||
private reconnectAttempts = 0;
|
private reconnectAttempts = 0;
|
||||||
private maxReconnectAttempts = 5;
|
private maxReconnectAttempts = 3;
|
||||||
private reconnectDelay = 1000;
|
|
||||||
private subscriptions = new Map<string, (data: any) => void>();
|
|
||||||
|
|
||||||
constructor(
|
constructor(url: string, token: string) {
|
||||||
private url: string,
|
|
||||||
private token: string,
|
|
||||||
private options: {
|
|
||||||
autoReconnect?: boolean;
|
|
||||||
maxReconnectAttempts?: number;
|
|
||||||
reconnectDelay?: number;
|
|
||||||
} = {},
|
|
||||||
) {
|
|
||||||
super();
|
super();
|
||||||
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
|
this.url = url;
|
||||||
this.reconnectDelay = options.reconnectDelay || 1000;
|
this.token = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async connect(): Promise<void> {
|
public async connect(): Promise<void> {
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
this.ws = new WebSocket(this.url);
|
this.ws = new WebSocket(this.url);
|
||||||
|
|
||||||
this.ws.on("open", () => {
|
this.ws.onopen = () => {
|
||||||
|
this.emit('connect');
|
||||||
this.authenticate();
|
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();
|
resolve();
|
||||||
});
|
};
|
||||||
|
|
||||||
this.once("auth_invalid", () => {
|
this.ws.onclose = () => {
|
||||||
reject(new Error("Authentication failed"));
|
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) {
|
} catch (error) {
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private authenticate(): void {
|
public isConnected(): boolean {
|
||||||
this.send({
|
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
||||||
type: "auth",
|
|
||||||
access_token: this.token,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleMessage(message: any): void {
|
public isAuthenticated(): boolean {
|
||||||
switch (message.type) {
|
return this.authenticated;
|
||||||
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 disconnect(): void {
|
public disconnect(): void {
|
||||||
if (this.ws) {
|
if (this.ws) {
|
||||||
this.ws.close();
|
this.ws.close();
|
||||||
this.ws = null;
|
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