@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,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AutomationOperations tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
+
import { AutomationOperations } from '../../src/haClient/automations.js';
|
|
7
|
+
import type { StateOperations } from '../../src/haClient/states.js';
|
|
8
|
+
import type { Entity } from '../../src/types/index.js';
|
|
9
|
+
|
|
10
|
+
describe('AutomationOperations', () => {
|
|
11
|
+
let mockStateOps: StateOperations;
|
|
12
|
+
let automationOps: AutomationOperations;
|
|
13
|
+
|
|
14
|
+
const mockAutomationEntities: Entity[] = [
|
|
15
|
+
{
|
|
16
|
+
entity_id: 'automation.morning_lights',
|
|
17
|
+
state: 'on',
|
|
18
|
+
attributes: {
|
|
19
|
+
friendly_name: 'Morning Lights',
|
|
20
|
+
last_triggered: '2026-01-08T06:00:00.000Z',
|
|
21
|
+
description: 'Turn on lights at sunrise',
|
|
22
|
+
mode: 'single',
|
|
23
|
+
},
|
|
24
|
+
last_changed: '2026-01-08T06:00:00.000Z',
|
|
25
|
+
last_updated: '2026-01-08T06:00:00.000Z',
|
|
26
|
+
context: { id: 'ctx-1', parent_id: null, user_id: null },
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
entity_id: 'automation.night_mode',
|
|
30
|
+
state: 'off',
|
|
31
|
+
attributes: {
|
|
32
|
+
friendly_name: 'Night Mode',
|
|
33
|
+
last_triggered: null,
|
|
34
|
+
mode: 'restart',
|
|
35
|
+
},
|
|
36
|
+
last_changed: '2026-01-07T22:00:00.000Z',
|
|
37
|
+
last_updated: '2026-01-08T10:00:00.000Z',
|
|
38
|
+
context: { id: 'ctx-2', parent_id: null, user_id: null },
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
entity_id: 'automation.motion_alert',
|
|
42
|
+
state: 'on',
|
|
43
|
+
attributes: {
|
|
44
|
+
friendly_name: 'Motion Alert',
|
|
45
|
+
last_triggered: '2026-01-08T09:30:00.000Z',
|
|
46
|
+
mode: 'queued',
|
|
47
|
+
},
|
|
48
|
+
last_changed: '2026-01-08T09:30:00.000Z',
|
|
49
|
+
last_updated: '2026-01-08T09:30:00.000Z',
|
|
50
|
+
context: { id: 'ctx-3', parent_id: null, user_id: null },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
entity_id: 'light.living_room', // Non-automation entity
|
|
54
|
+
state: 'on',
|
|
55
|
+
attributes: { friendly_name: 'Living Room Light' },
|
|
56
|
+
last_changed: '2026-01-08T10:00:00.000Z',
|
|
57
|
+
last_updated: '2026-01-08T10:00:00.000Z',
|
|
58
|
+
context: { id: 'ctx-4', parent_id: null, user_id: null },
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
mockStateOps = {
|
|
64
|
+
getStates: vi.fn().mockResolvedValue(mockAutomationEntities),
|
|
65
|
+
} as unknown as StateOperations;
|
|
66
|
+
|
|
67
|
+
automationOps = new AutomationOperations(mockStateOps);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('getAutomations', () => {
|
|
71
|
+
it('should return only automation entities', async () => {
|
|
72
|
+
const automations = await automationOps.getAutomations();
|
|
73
|
+
|
|
74
|
+
expect(automations).toHaveLength(3);
|
|
75
|
+
expect(automations.every(a => a.id.startsWith('automation.'))).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should map entity to Automation type', async () => {
|
|
79
|
+
const automations = await automationOps.getAutomations();
|
|
80
|
+
|
|
81
|
+
const morning = automations.find(a => a.id === 'automation.morning_lights');
|
|
82
|
+
expect(morning).toEqual({
|
|
83
|
+
id: 'automation.morning_lights',
|
|
84
|
+
alias: 'Morning Lights',
|
|
85
|
+
state: 'on',
|
|
86
|
+
last_triggered: '2026-01-08T06:00:00.000Z',
|
|
87
|
+
description: 'Turn on lights at sunrise',
|
|
88
|
+
mode: 'single',
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should handle automations without friendly_name', async () => {
|
|
93
|
+
(mockStateOps.getStates as ReturnType<typeof vi.fn>).mockResolvedValue([
|
|
94
|
+
{
|
|
95
|
+
entity_id: 'automation.unnamed',
|
|
96
|
+
state: 'on',
|
|
97
|
+
attributes: {},
|
|
98
|
+
last_changed: '2026-01-08T10:00:00.000Z',
|
|
99
|
+
last_updated: '2026-01-08T10:00:00.000Z',
|
|
100
|
+
context: { id: 'ctx-1', parent_id: null, user_id: null },
|
|
101
|
+
},
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const automations = await automationOps.getAutomations();
|
|
105
|
+
|
|
106
|
+
expect(automations[0].alias).toBe('unnamed');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should handle null last_triggered', async () => {
|
|
110
|
+
const automations = await automationOps.getAutomations();
|
|
111
|
+
|
|
112
|
+
const nightMode = automations.find(a => a.id === 'automation.night_mode');
|
|
113
|
+
expect(nightMode?.last_triggered).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should return empty array when no automations exist', async () => {
|
|
117
|
+
(mockStateOps.getStates as ReturnType<typeof vi.fn>).mockResolvedValue([
|
|
118
|
+
{
|
|
119
|
+
entity_id: 'light.test',
|
|
120
|
+
state: 'on',
|
|
121
|
+
attributes: {},
|
|
122
|
+
last_changed: '2026-01-08T10:00:00.000Z',
|
|
123
|
+
last_updated: '2026-01-08T10:00:00.000Z',
|
|
124
|
+
context: { id: 'ctx-1', parent_id: null, user_id: null },
|
|
125
|
+
},
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
const automations = await automationOps.getAutomations();
|
|
129
|
+
|
|
130
|
+
expect(automations).toEqual([]);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfigOperations tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
+
import { ConfigOperations } from '../../src/haClient/config.js';
|
|
7
|
+
import type { RequestHandler } from '../../src/haClient/request.js';
|
|
8
|
+
import type { HaVersion } from '../../src/types/index.js';
|
|
9
|
+
|
|
10
|
+
describe('ConfigOperations', () => {
|
|
11
|
+
let mockRequest: RequestHandler;
|
|
12
|
+
let configOps: ConfigOperations;
|
|
13
|
+
|
|
14
|
+
const mockConfig: HaVersion = {
|
|
15
|
+
version: '2026.1.0',
|
|
16
|
+
config_dir: '/config',
|
|
17
|
+
location_name: 'Home',
|
|
18
|
+
time_zone: 'Europe/London',
|
|
19
|
+
unit_system: {
|
|
20
|
+
length: 'km',
|
|
21
|
+
mass: 'kg',
|
|
22
|
+
temperature: '°C',
|
|
23
|
+
volume: 'L',
|
|
24
|
+
},
|
|
25
|
+
components: ['homeassistant', 'frontend', 'light', 'switch', 'sensor', 'automation'],
|
|
26
|
+
state: 'RUNNING',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
mockRequest = {
|
|
31
|
+
get: vi.fn(),
|
|
32
|
+
} as unknown as RequestHandler;
|
|
33
|
+
|
|
34
|
+
configOps = new ConfigOperations(mockRequest);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('getConfig', () => {
|
|
38
|
+
it('should fetch configuration', async () => {
|
|
39
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockResolvedValue(mockConfig);
|
|
40
|
+
|
|
41
|
+
const config = await configOps.getConfig();
|
|
42
|
+
|
|
43
|
+
expect(config).toEqual(mockConfig);
|
|
44
|
+
expect(mockRequest.get).toHaveBeenCalledWith('/config');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('checkApi', () => {
|
|
49
|
+
it('should return API health status', async () => {
|
|
50
|
+
const mockApiResponse = { message: 'API running.' };
|
|
51
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockResolvedValue(mockApiResponse);
|
|
52
|
+
|
|
53
|
+
const result = await configOps.checkApi();
|
|
54
|
+
|
|
55
|
+
expect(result).toEqual(mockApiResponse);
|
|
56
|
+
expect(mockRequest.get).toHaveBeenCalledWith('/');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('getVersion', () => {
|
|
61
|
+
it('should return version information', async () => {
|
|
62
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockResolvedValue(mockConfig);
|
|
63
|
+
|
|
64
|
+
const version = await configOps.getVersion();
|
|
65
|
+
|
|
66
|
+
expect(version).toEqual(mockConfig);
|
|
67
|
+
expect(version.version).toBe('2026.1.0');
|
|
68
|
+
expect(mockRequest.get).toHaveBeenCalledWith('/config');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should include all config fields', async () => {
|
|
72
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockResolvedValue(mockConfig);
|
|
73
|
+
|
|
74
|
+
const version = await configOps.getVersion();
|
|
75
|
+
|
|
76
|
+
expect(version.config_dir).toBe('/config');
|
|
77
|
+
expect(version.location_name).toBe('Home');
|
|
78
|
+
expect(version.time_zone).toBe('Europe/London');
|
|
79
|
+
expect(version.state).toBe('RUNNING');
|
|
80
|
+
expect(version.components).toContain('homeassistant');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should include unit_system', async () => {
|
|
84
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockResolvedValue(mockConfig);
|
|
85
|
+
|
|
86
|
+
const version = await configOps.getVersion();
|
|
87
|
+
|
|
88
|
+
expect(version.unit_system).toEqual({
|
|
89
|
+
length: 'km',
|
|
90
|
+
mass: 'kg',
|
|
91
|
+
temperature: '°C',
|
|
92
|
+
volume: 'L',
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EntityOperations tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
+
import { EntityOperations } from '../../src/haClient/entities.js';
|
|
7
|
+
import type { StateOperations } from '../../src/haClient/states.js';
|
|
8
|
+
import type { Entity } from '../../src/types/index.js';
|
|
9
|
+
|
|
10
|
+
describe('EntityOperations', () => {
|
|
11
|
+
let mockStateOps: StateOperations;
|
|
12
|
+
let entityOps: EntityOperations;
|
|
13
|
+
|
|
14
|
+
const mockEntities: Entity[] = [
|
|
15
|
+
{
|
|
16
|
+
entity_id: 'light.living_room',
|
|
17
|
+
state: 'on',
|
|
18
|
+
attributes: { friendly_name: 'Living Room Light', brightness: 255 },
|
|
19
|
+
last_changed: '2026-01-08T10:00:00.000Z',
|
|
20
|
+
last_updated: '2026-01-08T10:00:00.000Z',
|
|
21
|
+
context: { id: 'ctx-1', parent_id: null, user_id: null },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
entity_id: 'light.bedroom',
|
|
25
|
+
state: 'off',
|
|
26
|
+
attributes: { friendly_name: 'Bedroom Light' },
|
|
27
|
+
last_changed: '2026-01-08T09:00:00.000Z',
|
|
28
|
+
last_updated: '2026-01-08T10:00:00.000Z',
|
|
29
|
+
context: { id: 'ctx-2', parent_id: null, user_id: null },
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
entity_id: 'sensor.temperature',
|
|
33
|
+
state: '22.5',
|
|
34
|
+
attributes: { friendly_name: 'Temperature', unit_of_measurement: '°C' },
|
|
35
|
+
last_changed: '2026-01-08T10:00:00.000Z',
|
|
36
|
+
last_updated: '2026-01-08T10:00:00.000Z',
|
|
37
|
+
context: { id: 'ctx-3', parent_id: null, user_id: null },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
entity_id: 'switch.kitchen',
|
|
41
|
+
state: 'on',
|
|
42
|
+
attributes: { friendly_name: 'Kitchen Switch' },
|
|
43
|
+
last_changed: '2026-01-08T08:00:00.000Z',
|
|
44
|
+
last_updated: '2026-01-08T10:00:00.000Z',
|
|
45
|
+
context: { id: 'ctx-4', parent_id: null, user_id: null },
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
mockStateOps = {
|
|
51
|
+
getStates: vi.fn().mockResolvedValue(mockEntities),
|
|
52
|
+
} as unknown as StateOperations;
|
|
53
|
+
|
|
54
|
+
entityOps = new EntityOperations(mockStateOps);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('getEntitiesByDomain', () => {
|
|
58
|
+
it('should filter entities by domain', async () => {
|
|
59
|
+
const lights = await entityOps.getEntitiesByDomain('light');
|
|
60
|
+
|
|
61
|
+
expect(lights).toHaveLength(2);
|
|
62
|
+
expect(lights.map(e => e.entity_id)).toContain('light.living_room');
|
|
63
|
+
expect(lights.map(e => e.entity_id)).toContain('light.bedroom');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should return empty array for non-existent domain', async () => {
|
|
67
|
+
const climate = await entityOps.getEntitiesByDomain('climate');
|
|
68
|
+
|
|
69
|
+
expect(climate).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should return single entity domain', async () => {
|
|
73
|
+
const switches = await entityOps.getEntitiesByDomain('switch');
|
|
74
|
+
|
|
75
|
+
expect(switches).toHaveLength(1);
|
|
76
|
+
expect(switches[0].entity_id).toBe('switch.kitchen');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('searchEntities', () => {
|
|
81
|
+
it('should find entities by friendly name', async () => {
|
|
82
|
+
const results = await entityOps.searchEntities('living');
|
|
83
|
+
|
|
84
|
+
expect(results).toHaveLength(1);
|
|
85
|
+
expect(results[0].entity_id).toBe('light.living_room');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should find entities by entity_id', async () => {
|
|
89
|
+
const results = await entityOps.searchEntities('kitchen');
|
|
90
|
+
|
|
91
|
+
expect(results).toHaveLength(1);
|
|
92
|
+
expect(results[0].entity_id).toBe('switch.kitchen');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should be case insensitive', async () => {
|
|
96
|
+
const results = await entityOps.searchEntities('BEDROOM');
|
|
97
|
+
|
|
98
|
+
expect(results).toHaveLength(1);
|
|
99
|
+
expect(results[0].entity_id).toBe('light.bedroom');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should return multiple matches', async () => {
|
|
103
|
+
const results = await entityOps.searchEntities('light');
|
|
104
|
+
|
|
105
|
+
expect(results).toHaveLength(2);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should return empty array for no matches', async () => {
|
|
109
|
+
const results = await entityOps.searchEntities('nonexistent');
|
|
110
|
+
|
|
111
|
+
expect(results).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('getDomainSummary', () => {
|
|
116
|
+
it('should return domain summary with counts', async () => {
|
|
117
|
+
const summary = await entityOps.getDomainSummary('light');
|
|
118
|
+
|
|
119
|
+
expect(summary.domain).toBe('light');
|
|
120
|
+
expect(summary.count).toBe(2);
|
|
121
|
+
expect(summary.states).toEqual({ on: 1, off: 1 });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should include entity list with state and name', async () => {
|
|
125
|
+
const summary = await entityOps.getDomainSummary('light');
|
|
126
|
+
|
|
127
|
+
expect(summary.entities).toHaveLength(2);
|
|
128
|
+
expect(summary.entities).toContainEqual({
|
|
129
|
+
entity_id: 'light.living_room',
|
|
130
|
+
state: 'on',
|
|
131
|
+
friendly_name: 'Living Room Light',
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should return empty summary for non-existent domain', async () => {
|
|
136
|
+
const summary = await entityOps.getDomainSummary('climate');
|
|
137
|
+
|
|
138
|
+
expect(summary.domain).toBe('climate');
|
|
139
|
+
expect(summary.count).toBe(0);
|
|
140
|
+
expect(summary.states).toEqual({});
|
|
141
|
+
expect(summary.entities).toEqual([]);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('listEntities', () => {
|
|
146
|
+
it('should list all entities without filters', async () => {
|
|
147
|
+
const entities = await entityOps.listEntities();
|
|
148
|
+
|
|
149
|
+
expect(entities).toEqual(mockEntities);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should filter by domain', async () => {
|
|
153
|
+
const entities = await entityOps.listEntities({ domain: 'light' });
|
|
154
|
+
|
|
155
|
+
expect(entities).toHaveLength(2);
|
|
156
|
+
expect(entities.every(e => e.entity_id.startsWith('light.'))).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should filter by state', async () => {
|
|
160
|
+
const entities = await entityOps.listEntities({ state: 'on' });
|
|
161
|
+
|
|
162
|
+
expect(entities).toHaveLength(2);
|
|
163
|
+
expect(entities.every(e => e.state === 'on')).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should filter by search query', async () => {
|
|
167
|
+
const entities = await entityOps.listEntities({ search: 'room' });
|
|
168
|
+
|
|
169
|
+
expect(entities).toHaveLength(2); // living_room and bedroom
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should apply limit', async () => {
|
|
173
|
+
const entities = await entityOps.listEntities({ limit: 2 });
|
|
174
|
+
|
|
175
|
+
expect(entities).toHaveLength(2);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should combine multiple filters', async () => {
|
|
179
|
+
const entities = await entityOps.listEntities({
|
|
180
|
+
domain: 'light',
|
|
181
|
+
state: 'on',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(entities).toHaveLength(1);
|
|
185
|
+
expect(entities[0].entity_id).toBe('light.living_room');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HistoryOperations tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
+
import { HistoryOperations } from '../../src/haClient/history.js';
|
|
7
|
+
import type { RequestHandler } from '../../src/haClient/request.js';
|
|
8
|
+
import type { Entity, LogbookEntry } from '../../src/types/index.js';
|
|
9
|
+
|
|
10
|
+
describe('HistoryOperations', () => {
|
|
11
|
+
let mockRequest: RequestHandler;
|
|
12
|
+
let historyOps: HistoryOperations;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
mockRequest = {
|
|
16
|
+
get: vi.fn(),
|
|
17
|
+
} as unknown as RequestHandler;
|
|
18
|
+
|
|
19
|
+
historyOps = new HistoryOperations(mockRequest);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('getHistory', () => {
|
|
23
|
+
it('should fetch history for entity', async () => {
|
|
24
|
+
const mockHistory: Entity[][] = [
|
|
25
|
+
[
|
|
26
|
+
{
|
|
27
|
+
entity_id: 'sensor.temperature',
|
|
28
|
+
state: '20.0',
|
|
29
|
+
attributes: { unit_of_measurement: '°C' },
|
|
30
|
+
last_changed: '2026-01-08T08:00:00.000Z',
|
|
31
|
+
last_updated: '2026-01-08T08:00:00.000Z',
|
|
32
|
+
context: { id: 'ctx-1', parent_id: null, user_id: null },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
entity_id: 'sensor.temperature',
|
|
36
|
+
state: '21.5',
|
|
37
|
+
attributes: { unit_of_measurement: '°C' },
|
|
38
|
+
last_changed: '2026-01-08T09:00:00.000Z',
|
|
39
|
+
last_updated: '2026-01-08T09:00:00.000Z',
|
|
40
|
+
context: { id: 'ctx-2', parent_id: null, user_id: null },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
entity_id: 'sensor.temperature',
|
|
44
|
+
state: '22.5',
|
|
45
|
+
attributes: { unit_of_measurement: '°C' },
|
|
46
|
+
last_changed: '2026-01-08T10:00:00.000Z',
|
|
47
|
+
last_updated: '2026-01-08T10:00:00.000Z',
|
|
48
|
+
context: { id: 'ctx-3', parent_id: null, user_id: null },
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockResolvedValue(mockHistory);
|
|
54
|
+
|
|
55
|
+
const history = await historyOps.getHistory('sensor.temperature', 24);
|
|
56
|
+
|
|
57
|
+
expect(history).toEqual(mockHistory);
|
|
58
|
+
expect(mockRequest.get).toHaveBeenCalledWith(
|
|
59
|
+
expect.stringContaining('/history/period/'),
|
|
60
|
+
expect.objectContaining({
|
|
61
|
+
filter_entity_id: 'sensor.temperature',
|
|
62
|
+
end_time: expect.any(String),
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should use default 24 hours', async () => {
|
|
68
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockResolvedValue([[]]);
|
|
69
|
+
|
|
70
|
+
await historyOps.getHistory('sensor.test');
|
|
71
|
+
|
|
72
|
+
expect(mockRequest.get).toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should handle custom hours parameter', async () => {
|
|
76
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockResolvedValue([[]]);
|
|
77
|
+
|
|
78
|
+
await historyOps.getHistory('sensor.test', 48);
|
|
79
|
+
|
|
80
|
+
expect(mockRequest.get).toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should return empty history', async () => {
|
|
84
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockResolvedValue([[]]);
|
|
85
|
+
|
|
86
|
+
const history = await historyOps.getHistory('sensor.nonexistent');
|
|
87
|
+
|
|
88
|
+
expect(history).toEqual([[]]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('getSystemLog', () => {
|
|
93
|
+
it('should fetch logbook entries', async () => {
|
|
94
|
+
const mockEntries: LogbookEntry[] = [
|
|
95
|
+
{
|
|
96
|
+
when: '2026-01-08T10:00:00.000Z',
|
|
97
|
+
name: 'Living Room Light',
|
|
98
|
+
entity_id: 'light.living_room',
|
|
99
|
+
state: 'on',
|
|
100
|
+
domain: 'light',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
when: '2026-01-08T09:30:00.000Z',
|
|
104
|
+
name: 'Motion detected',
|
|
105
|
+
entity_id: 'binary_sensor.motion',
|
|
106
|
+
state: 'on',
|
|
107
|
+
domain: 'binary_sensor',
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockResolvedValue(mockEntries);
|
|
112
|
+
|
|
113
|
+
const entries = await historyOps.getSystemLog();
|
|
114
|
+
|
|
115
|
+
expect(entries).toEqual(mockEntries);
|
|
116
|
+
expect(mockRequest.get).toHaveBeenCalledWith(
|
|
117
|
+
expect.stringContaining('/logbook/'),
|
|
118
|
+
expect.objectContaining({
|
|
119
|
+
end_time: expect.any(String),
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should filter by entity_id', async () => {
|
|
125
|
+
const mockEntries: LogbookEntry[] = [
|
|
126
|
+
{
|
|
127
|
+
when: '2026-01-08T10:00:00.000Z',
|
|
128
|
+
name: 'Living Room Light',
|
|
129
|
+
entity_id: 'light.living_room',
|
|
130
|
+
state: 'on',
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockResolvedValue(mockEntries);
|
|
135
|
+
|
|
136
|
+
await historyOps.getSystemLog({ entity_id: 'light.living_room' });
|
|
137
|
+
|
|
138
|
+
expect(mockRequest.get).toHaveBeenCalledWith(
|
|
139
|
+
expect.any(String),
|
|
140
|
+
expect.objectContaining({
|
|
141
|
+
entity: 'light.living_room',
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should use custom hours parameter', async () => {
|
|
147
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
|
148
|
+
|
|
149
|
+
await historyOps.getSystemLog({ hours: 48 });
|
|
150
|
+
|
|
151
|
+
expect(mockRequest.get).toHaveBeenCalled();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should use default 24 hours', async () => {
|
|
155
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
|
156
|
+
|
|
157
|
+
await historyOps.getSystemLog();
|
|
158
|
+
|
|
159
|
+
expect(mockRequest.get).toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should return empty array when no logs', async () => {
|
|
163
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
|
164
|
+
|
|
165
|
+
const entries = await historyOps.getSystemLog();
|
|
166
|
+
|
|
167
|
+
expect(entries).toEqual([]);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|