@coffeerunhobby/mcp-ha-connect 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dockerignore +8 -0
- package/.env.example +149 -0
- package/.gitattributes +30 -0
- package/Dockerfile +33 -0
- package/LICENSE +21 -0
- package/README.md +394 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +183 -0
- package/dist/config.js.map +1 -0
- package/dist/haClient/automations.d.ts +67 -0
- package/dist/haClient/automations.d.ts.map +1 -0
- package/dist/haClient/automations.js +181 -0
- package/dist/haClient/automations.js.map +1 -0
- package/dist/haClient/config.d.ts +28 -0
- package/dist/haClient/config.d.ts.map +1 -0
- package/dist/haClient/config.js +38 -0
- package/dist/haClient/config.js.map +1 -0
- package/dist/haClient/devices.d.ts +219 -0
- package/dist/haClient/devices.d.ts.map +1 -0
- package/dist/haClient/devices.js +506 -0
- package/dist/haClient/devices.js.map +1 -0
- package/dist/haClient/entities.d.ts +35 -0
- package/dist/haClient/entities.d.ts.map +1 -0
- package/dist/haClient/entities.js +97 -0
- package/dist/haClient/entities.js.map +1 -0
- package/dist/haClient/events.d.ts +138 -0
- package/dist/haClient/events.d.ts.map +1 -0
- package/dist/haClient/events.js +344 -0
- package/dist/haClient/events.js.map +1 -0
- package/dist/haClient/history.d.ts +26 -0
- package/dist/haClient/history.d.ts.map +1 -0
- package/dist/haClient/history.js +54 -0
- package/dist/haClient/history.js.map +1 -0
- package/dist/haClient/index.d.ts +153 -0
- package/dist/haClient/index.d.ts.map +1 -0
- package/dist/haClient/index.js +223 -0
- package/dist/haClient/index.js.map +1 -0
- package/dist/haClient/request.d.ts +34 -0
- package/dist/haClient/request.d.ts.map +1 -0
- package/dist/haClient/request.js +93 -0
- package/dist/haClient/request.js.map +1 -0
- package/dist/haClient/services.d.ts +22 -0
- package/dist/haClient/services.d.ts.map +1 -0
- package/dist/haClient/services.js +39 -0
- package/dist/haClient/services.js.map +1 -0
- package/dist/haClient/states.d.ts +26 -0
- package/dist/haClient/states.d.ts.map +1 -0
- package/dist/haClient/states.js +53 -0
- package/dist/haClient/states.js.map +1 -0
- package/dist/haClient/updates.d.ts +22 -0
- package/dist/haClient/updates.d.ts.map +1 -0
- package/dist/haClient/updates.js +48 -0
- package/dist/haClient/updates.js.map +1 -0
- package/dist/haClient.d.ts +97 -0
- package/dist/haClient.d.ts.map +1 -0
- package/dist/haClient.js +344 -0
- package/dist/haClient.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +92 -0
- package/dist/index.js.map +1 -0
- package/dist/localAI/index.d.ts +37 -0
- package/dist/localAI/index.d.ts.map +1 -0
- package/dist/localAI/index.js +73 -0
- package/dist/localAI/index.js.map +1 -0
- package/dist/localAI/prompts.d.ts +20 -0
- package/dist/localAI/prompts.d.ts.map +1 -0
- package/dist/localAI/prompts.js +48 -0
- package/dist/localAI/prompts.js.map +1 -0
- package/dist/localAI/providers/base.d.ts +51 -0
- package/dist/localAI/providers/base.d.ts.map +1 -0
- package/dist/localAI/providers/base.js +80 -0
- package/dist/localAI/providers/base.js.map +1 -0
- package/dist/localAI/providers/ollama.d.ts +22 -0
- package/dist/localAI/providers/ollama.d.ts.map +1 -0
- package/dist/localAI/providers/ollama.js +63 -0
- package/dist/localAI/providers/ollama.js.map +1 -0
- package/dist/localAI/providers/openai.d.ts +23 -0
- package/dist/localAI/providers/openai.d.ts.map +1 -0
- package/dist/localAI/providers/openai.js +63 -0
- package/dist/localAI/providers/openai.js.map +1 -0
- package/dist/localAI/types.d.ts +61 -0
- package/dist/localAI/types.d.ts.map +1 -0
- package/dist/localAI/types.js +6 -0
- package/dist/localAI/types.js.map +1 -0
- package/dist/ollamaClient.d.ts +37 -0
- package/dist/ollamaClient.d.ts.map +1 -0
- package/dist/ollamaClient.js +110 -0
- package/dist/ollamaClient.js.map +1 -0
- package/dist/resources/index.d.ts +11 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +238 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/server/common.d.ts +9 -0
- package/dist/server/common.d.ts.map +1 -0
- package/dist/server/common.js +27 -0
- package/dist/server/common.js.map +1 -0
- package/dist/server/eventSubscription.d.ts +53 -0
- package/dist/server/eventSubscription.d.ts.map +1 -0
- package/dist/server/eventSubscription.js +257 -0
- package/dist/server/eventSubscription.js.map +1 -0
- package/dist/server/http.d.ts +12 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +491 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/rateLimiter.d.ts +70 -0
- package/dist/server/rateLimiter.d.ts.map +1 -0
- package/dist/server/rateLimiter.js +156 -0
- package/dist/server/rateLimiter.js.map +1 -0
- package/dist/server/sse.d.ts +32 -0
- package/dist/server/sse.d.ts.map +1 -0
- package/dist/server/sse.js +76 -0
- package/dist/server/sse.js.map +1 -0
- package/dist/server/stdio.d.ts +8 -0
- package/dist/server/stdio.d.ts.map +1 -0
- package/dist/server/stdio.js +16 -0
- package/dist/server/stdio.js.map +1 -0
- package/dist/server/stream.d.ts +26 -0
- package/dist/server/stream.d.ts.map +1 -0
- package/dist/server/stream.js +81 -0
- package/dist/server/stream.js.map +1 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +30 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/callService.d.ts +8 -0
- package/dist/tools/callService.d.ts.map +1 -0
- package/dist/tools/callService.js +100 -0
- package/dist/tools/callService.js.map +1 -0
- package/dist/tools/getClient.d.ts +8 -0
- package/dist/tools/getClient.d.ts.map +1 -0
- package/dist/tools/getClient.js +94 -0
- package/dist/tools/getClient.js.map +1 -0
- package/dist/tools/getDevice.d.ts +8 -0
- package/dist/tools/getDevice.d.ts.map +1 -0
- package/dist/tools/getDevice.js +94 -0
- package/dist/tools/getDevice.js.map +1 -0
- package/dist/tools/getEntitiesByDomain.d.ts +8 -0
- package/dist/tools/getEntitiesByDomain.d.ts.map +1 -0
- package/dist/tools/getEntitiesByDomain.js +80 -0
- package/dist/tools/getEntitiesByDomain.js.map +1 -0
- package/dist/tools/getState.d.ts +8 -0
- package/dist/tools/getState.d.ts.map +1 -0
- package/dist/tools/getState.js +90 -0
- package/dist/tools/getState.js.map +1 -0
- package/dist/tools/getStates.d.ts +8 -0
- package/dist/tools/getStates.d.ts.map +1 -0
- package/dist/tools/getStates.js +70 -0
- package/dist/tools/getStates.js.map +1 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/listClients.d.ts +8 -0
- package/dist/tools/listClients.d.ts.map +1 -0
- package/dist/tools/listClients.js +73 -0
- package/dist/tools/listClients.js.map +1 -0
- package/dist/tools/listDevices.d.ts +8 -0
- package/dist/tools/listDevices.d.ts.map +1 -0
- package/dist/tools/listDevices.js +73 -0
- package/dist/tools/listDevices.js.map +1 -0
- package/dist/tools/listSites.d.ts +8 -0
- package/dist/tools/listSites.d.ts.map +1 -0
- package/dist/tools/listSites.js +67 -0
- package/dist/tools/listSites.js.map +1 -0
- package/dist/tools/registry.d.ts +9 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +1382 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/searchDevices.d.ts +8 -0
- package/dist/tools/searchDevices.d.ts.map +1 -0
- package/dist/tools/searchDevices.js +84 -0
- package/dist/tools/searchDevices.js.map +1 -0
- package/dist/tools/searchEntities.d.ts +8 -0
- package/dist/tools/searchEntities.d.ts.map +1 -0
- package/dist/tools/searchEntities.js +80 -0
- package/dist/tools/searchEntities.js.map +1 -0
- package/dist/types/entity.d.ts +98 -0
- package/dist/types/entity.d.ts.map +1 -0
- package/dist/types/entity.js +2 -0
- package/dist/types/entity.js.map +1 -0
- package/dist/types/errors.d.ts +16 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +32 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/haConfig.d.ts +8 -0
- package/dist/types/haConfig.d.ts.map +1 -0
- package/dist/types/haConfig.js +2 -0
- package/dist/types/haConfig.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/request.d.ts +7 -0
- package/dist/types/request.d.ts.map +1 -0
- package/dist/types/request.js +2 -0
- package/dist/types/request.js.map +1 -0
- package/dist/types/serviceCall.d.ts +18 -0
- package/dist/types/serviceCall.d.ts.map +1 -0
- package/dist/types/serviceCall.js +2 -0
- package/dist/types/serviceCall.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/config-validations.d.ts +12 -0
- package/dist/utils/config-validations.d.ts.map +1 -0
- package/dist/utils/config-validations.js +43 -0
- package/dist/utils/config-validations.js.map +1 -0
- package/dist/utils/logger.d.ts +24 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +94 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/wrapper.d.ts +8 -0
- package/dist/wrapper.d.ts.map +1 -0
- package/dist/wrapper.js +301 -0
- package/dist/wrapper.js.map +1 -0
- package/docker-compose.yml +17 -0
- package/docs/CHANGELOG.md +86 -0
- package/docs/LOCAL_CLIENT.md +268 -0
- package/docs/SSE_API.md +341 -0
- package/eslint.config.js +33 -0
- package/n8n/ai-agent-safety-monitor.json +355 -0
- package/openapi.json +166 -0
- package/package.json +51 -0
- package/src/config.ts +260 -0
- package/src/haClient/automations.ts +217 -0
- package/src/haClient/config.ts +41 -0
- package/src/haClient/devices.ts +653 -0
- package/src/haClient/entities.ts +128 -0
- package/src/haClient/events.ts +456 -0
- package/src/haClient/history.ts +68 -0
- package/src/haClient/index.ts +287 -0
- package/src/haClient/request.ts +127 -0
- package/src/haClient/services.ts +45 -0
- package/src/haClient/states.ts +60 -0
- package/src/haClient/updates.ts +58 -0
- package/src/index.ts +99 -0
- package/src/localAI/index.ts +88 -0
- package/src/localAI/prompts.ts +50 -0
- package/src/localAI/providers/base.ts +120 -0
- package/src/localAI/providers/ollama.ts +78 -0
- package/src/localAI/providers/openai.ts +86 -0
- package/src/localAI/types.ts +67 -0
- package/src/resources/index.ts +266 -0
- package/src/server/common.ts +38 -0
- package/src/server/eventSubscription.ts +318 -0
- package/src/server/http.ts +546 -0
- package/src/server/rateLimiter.ts +195 -0
- package/src/server/sse.ts +120 -0
- package/src/server/stdio.ts +22 -0
- package/src/server/stream.ts +113 -0
- package/src/tools/callService.ts +114 -0
- package/src/tools/getEntitiesByDomain.ts +92 -0
- package/src/tools/getState.ts +103 -0
- package/src/tools/getStates.ts +80 -0
- package/src/tools/index.ts +9 -0
- package/src/tools/registry.ts +1521 -0
- package/src/tools/searchEntities.ts +92 -0
- package/src/types/entity.ts +110 -0
- package/src/types/errors.ts +33 -0
- package/src/types/haConfig.ts +7 -0
- package/src/types/index.ts +5 -0
- package/src/types/request.ts +6 -0
- package/src/types/serviceCall.ts +18 -0
- package/src/types.ts +2 -0
- package/src/utils/config-validations.ts +46 -0
- package/src/utils/logger.ts +114 -0
- package/src/wrapper.ts +340 -0
- package/tests/config.test.ts +332 -0
- package/tests/haClient/automations.test.ts +133 -0
- package/tests/haClient/config.test.ts +96 -0
- package/tests/haClient/entities.test.ts +188 -0
- package/tests/haClient/history.test.ts +170 -0
- package/tests/haClient/index.test.ts +221 -0
- package/tests/haClient/request.test.ts +217 -0
- package/tests/haClient/services.test.ts +120 -0
- package/tests/haClient/states.test.ts +119 -0
- package/tests/haClient/updates.test.ts +193 -0
- package/tests/index.test.ts +164 -0
- package/tests/localAI/index.test.ts +168 -0
- package/tests/localAI/ollama.test.ts +187 -0
- package/tests/localAI/openai.test.ts +257 -0
- package/tests/server/common.test.ts +64 -0
- package/tests/server/http.test.ts +326 -0
- package/tests/server/sse.test.ts +240 -0
- package/tests/server/stdio.test.ts +102 -0
- package/tests/server/stream.test.ts +287 -0
- package/tests/tools/registry.test.ts +355 -0
- package/tests/utils/config-validations.test.ts +289 -0
- package/tests/utils/logger.test.ts +327 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +26 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Home Assistant API Client
|
|
3
|
+
* Main client class that composes all operation modules
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { EnvironmentConfig } from '../config.js';
|
|
7
|
+
import type {
|
|
8
|
+
Entity,
|
|
9
|
+
ServiceCallData,
|
|
10
|
+
ServiceCallResponse,
|
|
11
|
+
Automation,
|
|
12
|
+
AutomationConfig,
|
|
13
|
+
DomainSummary,
|
|
14
|
+
HaVersion,
|
|
15
|
+
LogbookEntry,
|
|
16
|
+
UpdateInfo,
|
|
17
|
+
} from '../types/index.js';
|
|
18
|
+
import { logger } from '../utils/logger.js';
|
|
19
|
+
|
|
20
|
+
import { AutomationOperations } from './automations.js';
|
|
21
|
+
import { ConfigOperations } from './config.js';
|
|
22
|
+
import { DeviceOperations } from './devices.js';
|
|
23
|
+
import { EntityOperations } from './entities.js';
|
|
24
|
+
import { HistoryOperations } from './history.js';
|
|
25
|
+
import { RequestHandler } from './request.js';
|
|
26
|
+
import { ServiceOperations } from './services.js';
|
|
27
|
+
import { StateOperations } from './states.js';
|
|
28
|
+
import { UpdateOperations } from './updates.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Main client for interacting with the Home Assistant API.
|
|
32
|
+
* Organized with dedicated operation classes for each domain.
|
|
33
|
+
*/
|
|
34
|
+
export class HaClient {
|
|
35
|
+
private readonly request: RequestHandler;
|
|
36
|
+
private readonly stateOps: StateOperations;
|
|
37
|
+
private readonly serviceOps: ServiceOperations;
|
|
38
|
+
private readonly entityOps: EntityOperations;
|
|
39
|
+
private readonly automationOps: AutomationOperations;
|
|
40
|
+
private readonly historyOps: HistoryOperations;
|
|
41
|
+
private readonly updateOps: UpdateOperations;
|
|
42
|
+
private readonly configOps: ConfigOperations;
|
|
43
|
+
public readonly devices: DeviceOperations;
|
|
44
|
+
|
|
45
|
+
constructor(config: EnvironmentConfig) {
|
|
46
|
+
logger.debug('Initializing HaClient');
|
|
47
|
+
|
|
48
|
+
// Initialize request handler
|
|
49
|
+
this.request = new RequestHandler({
|
|
50
|
+
baseUrl: config.baseUrl,
|
|
51
|
+
token: config.token,
|
|
52
|
+
timeout: config.timeout,
|
|
53
|
+
strictSsl: config.strictSsl,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Initialize operation modules with dependencies
|
|
57
|
+
this.stateOps = new StateOperations(this.request);
|
|
58
|
+
this.serviceOps = new ServiceOperations(this.request);
|
|
59
|
+
this.entityOps = new EntityOperations(this.stateOps);
|
|
60
|
+
this.automationOps = new AutomationOperations(this.stateOps, this.serviceOps, this.request);
|
|
61
|
+
this.historyOps = new HistoryOperations(this.request);
|
|
62
|
+
this.updateOps = new UpdateOperations(this.entityOps);
|
|
63
|
+
this.configOps = new ConfigOperations(this.request);
|
|
64
|
+
this.devices = new DeviceOperations(this.serviceOps, this.stateOps);
|
|
65
|
+
|
|
66
|
+
logger.info('HaClient initialized');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ===== State Operations =====
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get all states (entities)
|
|
73
|
+
*/
|
|
74
|
+
async getStates(): Promise<Entity[]> {
|
|
75
|
+
return this.stateOps.getStates();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get state of a specific entity
|
|
80
|
+
*/
|
|
81
|
+
async getState(entityId: string): Promise<Entity | null> {
|
|
82
|
+
return this.stateOps.getState(entityId);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get all sensors (sensor.* and binary_sensor.* entities)
|
|
87
|
+
*/
|
|
88
|
+
async getAllSensors(): Promise<Entity[]> {
|
|
89
|
+
return this.stateOps.getAllSensors();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ===== Service Operations =====
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Call a service
|
|
96
|
+
*/
|
|
97
|
+
async callService(data: ServiceCallData): Promise<ServiceCallResponse> {
|
|
98
|
+
return this.serviceOps.callService(data);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Restart Home Assistant
|
|
103
|
+
*/
|
|
104
|
+
async restartServer(): Promise<void> {
|
|
105
|
+
return this.serviceOps.restartServer();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ===== Entity Operations =====
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get entities filtered by domain
|
|
112
|
+
*/
|
|
113
|
+
async getEntitiesByDomain(domain: string): Promise<Entity[]> {
|
|
114
|
+
return this.entityOps.getEntitiesByDomain(domain);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Search entities by name or entity_id
|
|
119
|
+
*/
|
|
120
|
+
async searchEntities(query: string): Promise<Entity[]> {
|
|
121
|
+
return this.entityOps.searchEntities(query);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get domain summary with entity counts and state breakdown
|
|
126
|
+
*/
|
|
127
|
+
async getDomainSummary(domain: string): Promise<DomainSummary> {
|
|
128
|
+
return this.entityOps.getDomainSummary(domain);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* List entities with optional filtering
|
|
133
|
+
*/
|
|
134
|
+
async listEntities(options: {
|
|
135
|
+
domain?: string;
|
|
136
|
+
search?: string;
|
|
137
|
+
limit?: number;
|
|
138
|
+
state?: string;
|
|
139
|
+
} = {}): Promise<Entity[]> {
|
|
140
|
+
return this.entityOps.listEntities(options);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ===== Automation Operations =====
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get all automations
|
|
147
|
+
*/
|
|
148
|
+
async getAutomations(): Promise<Automation[]> {
|
|
149
|
+
return this.automationOps.getAutomations();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get a single automation by entity_id
|
|
154
|
+
*/
|
|
155
|
+
async getAutomation(entityId: string): Promise<Automation | null> {
|
|
156
|
+
return this.automationOps.getAutomation(entityId);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Trigger an automation manually
|
|
161
|
+
*/
|
|
162
|
+
async triggerAutomation(entityId: string, variables?: Record<string, unknown>): Promise<ServiceCallResponse> {
|
|
163
|
+
return this.automationOps.triggerAutomation(entityId, variables);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Enable an automation
|
|
168
|
+
*/
|
|
169
|
+
async enableAutomation(entityId: string): Promise<ServiceCallResponse> {
|
|
170
|
+
return this.automationOps.enableAutomation(entityId);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Disable an automation
|
|
175
|
+
*/
|
|
176
|
+
async disableAutomation(entityId: string): Promise<ServiceCallResponse> {
|
|
177
|
+
return this.automationOps.disableAutomation(entityId);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Toggle an automation
|
|
182
|
+
*/
|
|
183
|
+
async toggleAutomation(entityId: string): Promise<ServiceCallResponse> {
|
|
184
|
+
return this.automationOps.toggleAutomation(entityId);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Reload all automations from configuration
|
|
189
|
+
*/
|
|
190
|
+
async reloadAutomations(): Promise<void> {
|
|
191
|
+
return this.automationOps.reloadAutomations();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Create a new automation
|
|
196
|
+
*/
|
|
197
|
+
async createAutomation(config: AutomationConfig): Promise<{ id: string }> {
|
|
198
|
+
return this.automationOps.createAutomation(config);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Update an existing automation
|
|
203
|
+
*/
|
|
204
|
+
async updateAutomation(automationId: string, config: Partial<AutomationConfig>): Promise<void> {
|
|
205
|
+
return this.automationOps.updateAutomation(automationId, config);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Delete an automation
|
|
210
|
+
*/
|
|
211
|
+
async deleteAutomation(automationId: string): Promise<void> {
|
|
212
|
+
return this.automationOps.deleteAutomation(automationId);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get automation execution trace
|
|
217
|
+
*/
|
|
218
|
+
async getAutomationTrace(entityId: string): Promise<unknown[]> {
|
|
219
|
+
return this.automationOps.getAutomationTrace(entityId);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ===== History Operations =====
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get historical data for an entity
|
|
226
|
+
*/
|
|
227
|
+
async getHistory(entityId: string, hours: number = 24): Promise<Entity[][]> {
|
|
228
|
+
return this.historyOps.getHistory(entityId, hours);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get system log entries from the logbook
|
|
233
|
+
*/
|
|
234
|
+
async getSystemLog(options: {
|
|
235
|
+
hours?: number;
|
|
236
|
+
entity_id?: string;
|
|
237
|
+
} = {}): Promise<LogbookEntry[]> {
|
|
238
|
+
return this.historyOps.getSystemLog(options);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ===== Update Operations =====
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get available updates by checking update.* entities
|
|
245
|
+
*/
|
|
246
|
+
async getAvailableUpdates(): Promise<{
|
|
247
|
+
updates: UpdateInfo[];
|
|
248
|
+
entities: Entity[];
|
|
249
|
+
}> {
|
|
250
|
+
return this.updateOps.getAvailableUpdates();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ===== Config Operations =====
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get config
|
|
257
|
+
*/
|
|
258
|
+
async getConfig(): Promise<Record<string, unknown>> {
|
|
259
|
+
return this.configOps.getConfig();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Check API (health check)
|
|
264
|
+
*/
|
|
265
|
+
async checkApi(): Promise<{ message: string }> {
|
|
266
|
+
return this.configOps.checkApi();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get Home Assistant version and configuration info
|
|
271
|
+
*/
|
|
272
|
+
async getVersion(): Promise<HaVersion> {
|
|
273
|
+
return this.configOps.getVersion();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Re-export operation classes for testing
|
|
278
|
+
export { RequestHandler } from './request.js';
|
|
279
|
+
export { StateOperations } from './states.js';
|
|
280
|
+
export { ServiceOperations } from './services.js';
|
|
281
|
+
export { EntityOperations } from './entities.js';
|
|
282
|
+
export { AutomationOperations } from './automations.js';
|
|
283
|
+
export { HistoryOperations } from './history.js';
|
|
284
|
+
export { UpdateOperations } from './updates.js';
|
|
285
|
+
export { ConfigOperations } from './config.js';
|
|
286
|
+
export { DeviceOperations } from './devices.js';
|
|
287
|
+
export type { LightControlOptions, ClimateControlOptions, MediaPlayerControlOptions, CoverControlOptions, FanControlOptions } from './devices.js';
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Request Handler for Home Assistant API
|
|
3
|
+
* Handles authentication and HTTP requests
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import https from 'https';
|
|
7
|
+
import type { RequestOptions } from '../types/index.js';
|
|
8
|
+
import { AuthenticationError, ApiError } from '../types/index.js';
|
|
9
|
+
import { logger } from '../utils/logger.js';
|
|
10
|
+
|
|
11
|
+
export interface RequestHandlerConfig {
|
|
12
|
+
baseUrl: string;
|
|
13
|
+
token: string;
|
|
14
|
+
timeout: number;
|
|
15
|
+
strictSsl: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* HTTP request handler for Home Assistant API calls.
|
|
20
|
+
*/
|
|
21
|
+
export class RequestHandler {
|
|
22
|
+
private httpsAgent?: https.Agent;
|
|
23
|
+
private baseUrl: string;
|
|
24
|
+
private token: string;
|
|
25
|
+
private timeout: number;
|
|
26
|
+
|
|
27
|
+
constructor(config: RequestHandlerConfig) {
|
|
28
|
+
this.baseUrl = config.baseUrl;
|
|
29
|
+
this.token = config.token;
|
|
30
|
+
this.timeout = config.timeout;
|
|
31
|
+
|
|
32
|
+
// Create HTTPS agent with custom SSL settings
|
|
33
|
+
if (!config.strictSsl) {
|
|
34
|
+
this.httpsAgent = new https.Agent({
|
|
35
|
+
rejectUnauthorized: false,
|
|
36
|
+
});
|
|
37
|
+
logger.warn('SSL certificate validation is disabled');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Make authenticated request to Home Assistant API
|
|
43
|
+
*/
|
|
44
|
+
async request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
|
45
|
+
const url = new URL(`${this.baseUrl}/api${path}`);
|
|
46
|
+
|
|
47
|
+
// Add query parameters
|
|
48
|
+
if (options.params) {
|
|
49
|
+
Object.entries(options.params).forEach(([key, value]) => {
|
|
50
|
+
url.searchParams.append(key, String(value));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const headers: Record<string, string> = {
|
|
55
|
+
'Authorization': `Bearer ${this.token}`,
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
...options.headers,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
logger.debug('Making API request', {
|
|
61
|
+
method: options.method ?? 'GET',
|
|
62
|
+
path,
|
|
63
|
+
params: options.params,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(url.toString(), {
|
|
68
|
+
method: options.method ?? 'GET',
|
|
69
|
+
headers,
|
|
70
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
71
|
+
// @ts-expect-error - agent is valid but not in types
|
|
72
|
+
agent: this.httpsAgent,
|
|
73
|
+
signal: AbortSignal.timeout(this.timeout),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
const errorText = await response.text();
|
|
78
|
+
|
|
79
|
+
if (response.status === 401) {
|
|
80
|
+
throw new AuthenticationError(
|
|
81
|
+
'Invalid Home Assistant token',
|
|
82
|
+
{ status: response.status, body: errorText }
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw new ApiError(
|
|
87
|
+
`API request failed: ${response.statusText}`,
|
|
88
|
+
response.status,
|
|
89
|
+
{ path, body: errorText }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Handle empty responses
|
|
94
|
+
const contentType = response.headers.get('content-type');
|
|
95
|
+
if (!contentType?.includes('application/json')) {
|
|
96
|
+
return {} as T;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const data = await response.json();
|
|
100
|
+
return data as T;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error instanceof AuthenticationError || error instanceof ApiError) {
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw new ApiError(
|
|
107
|
+
`Request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
108
|
+
500,
|
|
109
|
+
{ path }
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Make a GET request
|
|
116
|
+
*/
|
|
117
|
+
async get<T>(path: string, params?: Record<string, string | number>): Promise<T> {
|
|
118
|
+
return this.request<T>(path, { params });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Make a POST request
|
|
123
|
+
*/
|
|
124
|
+
async post<T>(path: string, body?: unknown): Promise<T> {
|
|
125
|
+
return this.request<T>(path, { method: 'POST', body });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Operations for Home Assistant API
|
|
3
|
+
* Handles service calls and server management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ServiceCallData, ServiceCallResponse } from '../types/index.js';
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
import type { RequestHandler } from './request.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Service-related operations for the Home Assistant API.
|
|
12
|
+
*/
|
|
13
|
+
export class ServiceOperations {
|
|
14
|
+
constructor(private readonly request: RequestHandler) {}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Call a service
|
|
18
|
+
*/
|
|
19
|
+
async callService(data: ServiceCallData): Promise<ServiceCallResponse> {
|
|
20
|
+
logger.info('Calling service', {
|
|
21
|
+
domain: data.domain,
|
|
22
|
+
service: data.service,
|
|
23
|
+
hasTarget: !!data.target,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const path = `/services/${data.domain}/${data.service}`;
|
|
27
|
+
const body = {
|
|
28
|
+
...data.service_data,
|
|
29
|
+
...data.target,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return await this.request.post<ServiceCallResponse>(path, body);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Restart Home Assistant
|
|
37
|
+
*/
|
|
38
|
+
async restartServer(): Promise<void> {
|
|
39
|
+
logger.warn('Restarting Home Assistant');
|
|
40
|
+
|
|
41
|
+
await this.request.post('/services/homeassistant/restart', {});
|
|
42
|
+
|
|
43
|
+
logger.info('Home Assistant restart initiated');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Operations for Home Assistant API
|
|
3
|
+
* Handles entity state retrieval
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Entity } from '../types/index.js';
|
|
7
|
+
import { ApiError } from '../types/index.js';
|
|
8
|
+
import { logger } from '../utils/logger.js';
|
|
9
|
+
import type { RequestHandler } from './request.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* State-related operations for the Home Assistant API.
|
|
13
|
+
*/
|
|
14
|
+
export class StateOperations {
|
|
15
|
+
constructor(private readonly request: RequestHandler) {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get all states (entities)
|
|
19
|
+
*/
|
|
20
|
+
async getStates(): Promise<Entity[]> {
|
|
21
|
+
logger.debug('Fetching all states');
|
|
22
|
+
const states = await this.request.get<Entity[]>('/states');
|
|
23
|
+
logger.info('Fetched states', { count: states.length });
|
|
24
|
+
return states;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get state of a specific entity
|
|
29
|
+
*/
|
|
30
|
+
async getState(entityId: string): Promise<Entity | null> {
|
|
31
|
+
logger.debug('Fetching state', { entityId });
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const state = await this.request.get<Entity>(`/states/${entityId}`);
|
|
35
|
+
return state;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error instanceof ApiError && error.statusCode === 404) {
|
|
38
|
+
logger.warn('Entity not found', { entityId });
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get all sensors (sensor.* and binary_sensor.* entities)
|
|
47
|
+
*/
|
|
48
|
+
async getAllSensors(): Promise<Entity[]> {
|
|
49
|
+
logger.debug('Fetching all sensors');
|
|
50
|
+
|
|
51
|
+
const allStates = await this.getStates();
|
|
52
|
+
const sensors = allStates.filter(entity =>
|
|
53
|
+
entity.entity_id.startsWith('sensor.') ||
|
|
54
|
+
entity.entity_id.startsWith('binary_sensor.')
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
logger.info('Fetched sensors', { count: sensors.length });
|
|
58
|
+
return sensors;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update Operations for Home Assistant API
|
|
3
|
+
* Handles checking for available updates
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Entity, UpdateInfo } from '../types/index.js';
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
import type { EntityOperations } from './entities.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Update-related operations for the Home Assistant API.
|
|
12
|
+
*/
|
|
13
|
+
export class UpdateOperations {
|
|
14
|
+
constructor(private readonly entityOps: EntityOperations) {}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get available updates by checking update.* entities
|
|
18
|
+
* Works with regular long-lived access tokens
|
|
19
|
+
*/
|
|
20
|
+
async getAvailableUpdates(): Promise<{
|
|
21
|
+
updates: UpdateInfo[];
|
|
22
|
+
entities: Entity[];
|
|
23
|
+
}> {
|
|
24
|
+
logger.debug('Fetching available updates from update entities');
|
|
25
|
+
|
|
26
|
+
// Get all update entities
|
|
27
|
+
const updateEntities = await this.entityOps.getEntitiesByDomain('update');
|
|
28
|
+
|
|
29
|
+
// Filter to only those with updates available (state = 'on')
|
|
30
|
+
const updates: UpdateInfo[] = [];
|
|
31
|
+
|
|
32
|
+
for (const entity of updateEntities) {
|
|
33
|
+
const attrs = entity.attributes;
|
|
34
|
+
const installedVersion = attrs.installed_version as string | undefined;
|
|
35
|
+
const latestVersion = attrs.latest_version as string | undefined;
|
|
36
|
+
const title = attrs.title as string || attrs.friendly_name as string || entity.entity_id;
|
|
37
|
+
|
|
38
|
+
// state 'on' means update is available
|
|
39
|
+
if (entity.state === 'on' && installedVersion && latestVersion) {
|
|
40
|
+
updates.push({
|
|
41
|
+
update_type: entity.entity_id.includes('core') ? 'core' :
|
|
42
|
+
entity.entity_id.includes('supervisor') ? 'supervisor' :
|
|
43
|
+
entity.entity_id.includes('operating_system') ? 'os' : 'addon',
|
|
44
|
+
name: title,
|
|
45
|
+
version_current: installedVersion,
|
|
46
|
+
version_latest: latestVersion,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
logger.info('Fetched available updates', { updateCount: updates.length, totalEntities: updateEntities.length });
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
updates,
|
|
55
|
+
entities: updateEntities,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Home Assistant MCP Server
|
|
4
|
+
* Entry point for the Model Context Protocol server
|
|
5
|
+
* Supports multiple transport modes: stdio, HTTP+SSE, HTTP+Stream
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { loadConfig } from './config.js';
|
|
9
|
+
import { HaClient } from './haClient/index.js';
|
|
10
|
+
import { LocalAIClient } from './localAI/index.js';
|
|
11
|
+
import { startHttpServer } from './server/http.js';
|
|
12
|
+
import { startStdioServer } from './server/stdio.js';
|
|
13
|
+
import { initLogger, logger } from './utils/logger.js';
|
|
14
|
+
|
|
15
|
+
async function main(): Promise<void> {
|
|
16
|
+
try {
|
|
17
|
+
// Load and validate configuration
|
|
18
|
+
const config = loadConfig();
|
|
19
|
+
|
|
20
|
+
// When running in stdio mode, logs must go to stderr to avoid interfering with MCP protocol on stdout
|
|
21
|
+
const useStderr = !config.useHttp;
|
|
22
|
+
|
|
23
|
+
// Initialize logger with configured level, format, and output stream
|
|
24
|
+
initLogger(config.logLevel, config.logFormat, useStderr);
|
|
25
|
+
|
|
26
|
+
logger.info('Loaded Home Assistant configuration', {
|
|
27
|
+
baseUrl: config.baseUrl,
|
|
28
|
+
hasToken: !!config.token,
|
|
29
|
+
strictSsl: config.strictSsl,
|
|
30
|
+
timeout: config.timeout,
|
|
31
|
+
useHttp: config.useHttp,
|
|
32
|
+
httpTransport: config.useHttp ? config.httpTransport : undefined,
|
|
33
|
+
aiProvider: config.aiProvider,
|
|
34
|
+
aiUrl: config.aiUrl,
|
|
35
|
+
aiModel: config.aiModel,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Create Home Assistant client
|
|
39
|
+
const client = new HaClient(config);
|
|
40
|
+
|
|
41
|
+
// Test Home Assistant connection
|
|
42
|
+
const apiCheck = await client.checkApi();
|
|
43
|
+
logger.info('Connected to Home Assistant', { message: apiCheck.message });
|
|
44
|
+
|
|
45
|
+
// Create Local AI client for AI analysis (unless explicitly disabled)
|
|
46
|
+
let aiClient: LocalAIClient | undefined;
|
|
47
|
+
if (config.aiProvider === 'none') {
|
|
48
|
+
logger.info('AI features disabled (AI_PROVIDER=none)');
|
|
49
|
+
} else {
|
|
50
|
+
aiClient = new LocalAIClient(config);
|
|
51
|
+
const aiHealthy = await aiClient.checkHealth();
|
|
52
|
+
if (aiHealthy) {
|
|
53
|
+
logger.info('Connected to AI provider', {
|
|
54
|
+
provider: aiClient.getProviderName(),
|
|
55
|
+
url: config.aiUrl,
|
|
56
|
+
model: config.aiModel,
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
logger.warn('AI provider not available - AI analysis tool will be disabled', {
|
|
60
|
+
provider: config.aiProvider,
|
|
61
|
+
url: config.aiUrl,
|
|
62
|
+
});
|
|
63
|
+
aiClient = undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Start server in appropriate mode
|
|
68
|
+
if (config.useHttp) {
|
|
69
|
+
await startHttpServer(client, config, aiClient);
|
|
70
|
+
} else {
|
|
71
|
+
await startStdioServer(client, aiClient);
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
75
|
+
logger.error('Failed to start Home Assistant MCP server', {
|
|
76
|
+
error: message,
|
|
77
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
78
|
+
});
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Handle graceful shutdown
|
|
84
|
+
process.on('SIGINT', () => {
|
|
85
|
+
logger.info('Received SIGINT, shutting down gracefully');
|
|
86
|
+
process.exit(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
process.on('SIGTERM', () => {
|
|
90
|
+
logger.info('Received SIGTERM, shutting down gracefully');
|
|
91
|
+
process.exit(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Start the server
|
|
95
|
+
main().catch((error) => {
|
|
96
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
97
|
+
logger.error('Unhandled error in main', { error: message });
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
});
|