@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,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HaClient aggregator tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import type { EnvironmentConfig } from '../../src/config.js';
|
|
7
|
+
|
|
8
|
+
// Mock all operation modules
|
|
9
|
+
const statesModule = vi.hoisted(() => {
|
|
10
|
+
const instance = {
|
|
11
|
+
getStates: vi.fn().mockResolvedValue([{ entity_id: 'light.test' }]),
|
|
12
|
+
getState: vi.fn().mockResolvedValue({ entity_id: 'light.test' }),
|
|
13
|
+
getAllSensors: vi.fn().mockResolvedValue([{ entity_id: 'sensor.test' }]),
|
|
14
|
+
};
|
|
15
|
+
const StateOperations = vi.fn(() => instance);
|
|
16
|
+
return { instance, StateOperations };
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const servicesModule = vi.hoisted(() => {
|
|
20
|
+
const instance = {
|
|
21
|
+
callService: vi.fn().mockResolvedValue({ context: { id: 'ctx-1' } }),
|
|
22
|
+
restartServer: vi.fn().mockResolvedValue(undefined),
|
|
23
|
+
};
|
|
24
|
+
const ServiceOperations = vi.fn(() => instance);
|
|
25
|
+
return { instance, ServiceOperations };
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const entitiesModule = vi.hoisted(() => {
|
|
29
|
+
const instance = {
|
|
30
|
+
getEntitiesByDomain: vi.fn().mockResolvedValue([{ entity_id: 'light.test' }]),
|
|
31
|
+
searchEntities: vi.fn().mockResolvedValue([{ entity_id: 'light.test' }]),
|
|
32
|
+
getDomainSummary: vi.fn().mockResolvedValue({ domain: 'light', count: 1 }),
|
|
33
|
+
listEntities: vi.fn().mockResolvedValue([{ entity_id: 'light.test' }]),
|
|
34
|
+
};
|
|
35
|
+
const EntityOperations = vi.fn(() => instance);
|
|
36
|
+
return { instance, EntityOperations };
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const automationsModule = vi.hoisted(() => {
|
|
40
|
+
const instance = {
|
|
41
|
+
getAutomations: vi.fn().mockResolvedValue([{ id: 'automation.test' }]),
|
|
42
|
+
};
|
|
43
|
+
const AutomationOperations = vi.fn(() => instance);
|
|
44
|
+
return { instance, AutomationOperations };
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const historyModule = vi.hoisted(() => {
|
|
48
|
+
const instance = {
|
|
49
|
+
getHistory: vi.fn().mockResolvedValue([[{ entity_id: 'sensor.test' }]]),
|
|
50
|
+
getSystemLog: vi.fn().mockResolvedValue([{ when: '2026-01-08T10:00:00Z' }]),
|
|
51
|
+
};
|
|
52
|
+
const HistoryOperations = vi.fn(() => instance);
|
|
53
|
+
return { instance, HistoryOperations };
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const updatesModule = vi.hoisted(() => {
|
|
57
|
+
const instance = {
|
|
58
|
+
getAvailableUpdates: vi.fn().mockResolvedValue({ updates: [], entities: [] }),
|
|
59
|
+
};
|
|
60
|
+
const UpdateOperations = vi.fn(() => instance);
|
|
61
|
+
return { instance, UpdateOperations };
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const configModule = vi.hoisted(() => {
|
|
65
|
+
const instance = {
|
|
66
|
+
getConfig: vi.fn().mockResolvedValue({ version: '2026.1.0' }),
|
|
67
|
+
checkApi: vi.fn().mockResolvedValue({ message: 'API running.' }),
|
|
68
|
+
getVersion: vi.fn().mockResolvedValue({ version: '2026.1.0' }),
|
|
69
|
+
};
|
|
70
|
+
const ConfigOperations = vi.fn(() => instance);
|
|
71
|
+
return { instance, ConfigOperations };
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const requestModule = vi.hoisted(() => {
|
|
75
|
+
const instance = {
|
|
76
|
+
request: vi.fn().mockResolvedValue({}),
|
|
77
|
+
get: vi.fn().mockResolvedValue({}),
|
|
78
|
+
post: vi.fn().mockResolvedValue({}),
|
|
79
|
+
};
|
|
80
|
+
const RequestHandler = vi.fn(() => instance);
|
|
81
|
+
return { instance, RequestHandler };
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Apply mocks
|
|
85
|
+
vi.mock('../../src/haClient/states.js', () => ({ StateOperations: statesModule.StateOperations }));
|
|
86
|
+
vi.mock('../../src/haClient/services.js', () => ({ ServiceOperations: servicesModule.ServiceOperations }));
|
|
87
|
+
vi.mock('../../src/haClient/entities.js', () => ({ EntityOperations: entitiesModule.EntityOperations }));
|
|
88
|
+
vi.mock('../../src/haClient/automations.js', () => ({ AutomationOperations: automationsModule.AutomationOperations }));
|
|
89
|
+
vi.mock('../../src/haClient/history.js', () => ({ HistoryOperations: historyModule.HistoryOperations }));
|
|
90
|
+
vi.mock('../../src/haClient/updates.js', () => ({ UpdateOperations: updatesModule.UpdateOperations }));
|
|
91
|
+
vi.mock('../../src/haClient/config.js', () => ({ ConfigOperations: configModule.ConfigOperations }));
|
|
92
|
+
vi.mock('../../src/haClient/request.js', () => ({ RequestHandler: requestModule.RequestHandler }));
|
|
93
|
+
|
|
94
|
+
const baseConfig: EnvironmentConfig = {
|
|
95
|
+
baseUrl: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
96
|
+
token: 'test-token',
|
|
97
|
+
strictSsl: false,
|
|
98
|
+
timeout: 30000,
|
|
99
|
+
aiProvider: 'ollama',
|
|
100
|
+
aiUrl: 'http://ollama.10.0.0.17.nip.io:11434',
|
|
101
|
+
aiModel: 'phi4:14b',
|
|
102
|
+
aiTimeout: 60000,
|
|
103
|
+
logLevel: 'error',
|
|
104
|
+
logFormat: 'plain',
|
|
105
|
+
useHttp: false,
|
|
106
|
+
stateful: false,
|
|
107
|
+
httpTransport: 'stream',
|
|
108
|
+
httpEnableHealthcheck: true,
|
|
109
|
+
httpAllowCors: true,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
describe('HaClient aggregator', () => {
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
vi.clearAllMocks();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('initializes all operation modules', async () => {
|
|
118
|
+
const { HaClient } = await import('../../src/haClient/index.js');
|
|
119
|
+
const client = new HaClient(baseConfig);
|
|
120
|
+
|
|
121
|
+
expect(requestModule.RequestHandler).toHaveBeenCalledWith(
|
|
122
|
+
expect.objectContaining({
|
|
123
|
+
baseUrl: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
124
|
+
token: 'test-token',
|
|
125
|
+
})
|
|
126
|
+
);
|
|
127
|
+
expect(statesModule.StateOperations).toHaveBeenCalledWith(requestModule.instance);
|
|
128
|
+
expect(servicesModule.ServiceOperations).toHaveBeenCalledWith(requestModule.instance);
|
|
129
|
+
expect(entitiesModule.EntityOperations).toHaveBeenCalledWith(statesModule.instance);
|
|
130
|
+
expect(automationsModule.AutomationOperations).toHaveBeenCalledWith(
|
|
131
|
+
statesModule.instance,
|
|
132
|
+
servicesModule.instance,
|
|
133
|
+
requestModule.instance
|
|
134
|
+
);
|
|
135
|
+
expect(historyModule.HistoryOperations).toHaveBeenCalledWith(requestModule.instance);
|
|
136
|
+
expect(updatesModule.UpdateOperations).toHaveBeenCalledWith(entitiesModule.instance);
|
|
137
|
+
expect(configModule.ConfigOperations).toHaveBeenCalledWith(requestModule.instance);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('proxies state operations', async () => {
|
|
141
|
+
const { HaClient } = await import('../../src/haClient/index.js');
|
|
142
|
+
const client = new HaClient(baseConfig);
|
|
143
|
+
|
|
144
|
+
await expect(client.getStates()).resolves.toEqual([{ entity_id: 'light.test' }]);
|
|
145
|
+
await expect(client.getState('light.test')).resolves.toEqual({ entity_id: 'light.test' });
|
|
146
|
+
await expect(client.getAllSensors()).resolves.toEqual([{ entity_id: 'sensor.test' }]);
|
|
147
|
+
|
|
148
|
+
expect(statesModule.instance.getStates).toHaveBeenCalled();
|
|
149
|
+
expect(statesModule.instance.getState).toHaveBeenCalledWith('light.test');
|
|
150
|
+
expect(statesModule.instance.getAllSensors).toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('proxies service operations', async () => {
|
|
154
|
+
const { HaClient } = await import('../../src/haClient/index.js');
|
|
155
|
+
const client = new HaClient(baseConfig);
|
|
156
|
+
|
|
157
|
+
const serviceData = { domain: 'light', service: 'turn_on' };
|
|
158
|
+
await expect(client.callService(serviceData)).resolves.toEqual({ context: { id: 'ctx-1' } });
|
|
159
|
+
await expect(client.restartServer()).resolves.toBeUndefined();
|
|
160
|
+
|
|
161
|
+
expect(servicesModule.instance.callService).toHaveBeenCalledWith(serviceData);
|
|
162
|
+
expect(servicesModule.instance.restartServer).toHaveBeenCalled();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('proxies entity operations', async () => {
|
|
166
|
+
const { HaClient } = await import('../../src/haClient/index.js');
|
|
167
|
+
const client = new HaClient(baseConfig);
|
|
168
|
+
|
|
169
|
+
await expect(client.getEntitiesByDomain('light')).resolves.toEqual([{ entity_id: 'light.test' }]);
|
|
170
|
+
await expect(client.searchEntities('test')).resolves.toEqual([{ entity_id: 'light.test' }]);
|
|
171
|
+
await expect(client.getDomainSummary('light')).resolves.toEqual({ domain: 'light', count: 1 });
|
|
172
|
+
await expect(client.listEntities({ domain: 'light' })).resolves.toEqual([{ entity_id: 'light.test' }]);
|
|
173
|
+
|
|
174
|
+
expect(entitiesModule.instance.getEntitiesByDomain).toHaveBeenCalledWith('light');
|
|
175
|
+
expect(entitiesModule.instance.searchEntities).toHaveBeenCalledWith('test');
|
|
176
|
+
expect(entitiesModule.instance.getDomainSummary).toHaveBeenCalledWith('light');
|
|
177
|
+
expect(entitiesModule.instance.listEntities).toHaveBeenCalledWith({ domain: 'light' });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('proxies automation operations', async () => {
|
|
181
|
+
const { HaClient } = await import('../../src/haClient/index.js');
|
|
182
|
+
const client = new HaClient(baseConfig);
|
|
183
|
+
|
|
184
|
+
await expect(client.getAutomations()).resolves.toEqual([{ id: 'automation.test' }]);
|
|
185
|
+
|
|
186
|
+
expect(automationsModule.instance.getAutomations).toHaveBeenCalled();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('proxies history operations', async () => {
|
|
190
|
+
const { HaClient } = await import('../../src/haClient/index.js');
|
|
191
|
+
const client = new HaClient(baseConfig);
|
|
192
|
+
|
|
193
|
+
await expect(client.getHistory('sensor.test', 24)).resolves.toEqual([[{ entity_id: 'sensor.test' }]]);
|
|
194
|
+
await expect(client.getSystemLog({ hours: 24 })).resolves.toEqual([{ when: '2026-01-08T10:00:00Z' }]);
|
|
195
|
+
|
|
196
|
+
expect(historyModule.instance.getHistory).toHaveBeenCalledWith('sensor.test', 24);
|
|
197
|
+
expect(historyModule.instance.getSystemLog).toHaveBeenCalledWith({ hours: 24 });
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('proxies update operations', async () => {
|
|
201
|
+
const { HaClient } = await import('../../src/haClient/index.js');
|
|
202
|
+
const client = new HaClient(baseConfig);
|
|
203
|
+
|
|
204
|
+
await expect(client.getAvailableUpdates()).resolves.toEqual({ updates: [], entities: [] });
|
|
205
|
+
|
|
206
|
+
expect(updatesModule.instance.getAvailableUpdates).toHaveBeenCalled();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('proxies config operations', async () => {
|
|
210
|
+
const { HaClient } = await import('../../src/haClient/index.js');
|
|
211
|
+
const client = new HaClient(baseConfig);
|
|
212
|
+
|
|
213
|
+
await expect(client.getConfig()).resolves.toEqual({ version: '2026.1.0' });
|
|
214
|
+
await expect(client.checkApi()).resolves.toEqual({ message: 'API running.' });
|
|
215
|
+
await expect(client.getVersion()).resolves.toEqual({ version: '2026.1.0' });
|
|
216
|
+
|
|
217
|
+
expect(configModule.instance.getConfig).toHaveBeenCalled();
|
|
218
|
+
expect(configModule.instance.checkApi).toHaveBeenCalled();
|
|
219
|
+
expect(configModule.instance.getVersion).toHaveBeenCalled();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RequestHandler tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import { RequestHandler } from '../../src/haClient/request.js';
|
|
7
|
+
import { AuthenticationError, ApiError } from '../../src/types/index.js';
|
|
8
|
+
|
|
9
|
+
// Mock fetch globally
|
|
10
|
+
global.fetch = vi.fn();
|
|
11
|
+
|
|
12
|
+
describe('RequestHandler', () => {
|
|
13
|
+
let handler: RequestHandler;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
handler = new RequestHandler({
|
|
17
|
+
baseUrl: 'http://homeassistant.10.0.0.19.nip.io:8123',
|
|
18
|
+
token: 'test-token-12345',
|
|
19
|
+
timeout: 30000,
|
|
20
|
+
strictSsl: false,
|
|
21
|
+
});
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('request', () => {
|
|
30
|
+
it('should make GET request with authorization header', async () => {
|
|
31
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
32
|
+
ok: true,
|
|
33
|
+
headers: new Map([['content-type', 'application/json']]),
|
|
34
|
+
json: async () => ({ data: 'test' }),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await handler.request('/test');
|
|
38
|
+
|
|
39
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
40
|
+
'http://homeassistant.10.0.0.19.nip.io:8123/api/test',
|
|
41
|
+
expect.objectContaining({
|
|
42
|
+
method: 'GET',
|
|
43
|
+
headers: expect.objectContaining({
|
|
44
|
+
'Authorization': 'Bearer test-token-12345',
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
}),
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should make POST request with body', async () => {
|
|
52
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
53
|
+
ok: true,
|
|
54
|
+
headers: new Map([['content-type', 'application/json']]),
|
|
55
|
+
json: async () => ({ success: true }),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await handler.request('/test', {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
body: { key: 'value' },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
64
|
+
'http://homeassistant.10.0.0.19.nip.io:8123/api/test',
|
|
65
|
+
expect.objectContaining({
|
|
66
|
+
method: 'POST',
|
|
67
|
+
body: JSON.stringify({ key: 'value' }),
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should add query parameters to URL', async () => {
|
|
73
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
74
|
+
ok: true,
|
|
75
|
+
headers: new Map([['content-type', 'application/json']]),
|
|
76
|
+
json: async () => ({}),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await handler.request('/test', {
|
|
80
|
+
params: { filter: 'active', limit: 10 },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
84
|
+
expect.stringContaining('filter=active'),
|
|
85
|
+
expect.any(Object)
|
|
86
|
+
);
|
|
87
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
88
|
+
expect.stringContaining('limit=10'),
|
|
89
|
+
expect.any(Object)
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should throw AuthenticationError on 401', async () => {
|
|
94
|
+
const mockResponse = {
|
|
95
|
+
ok: false,
|
|
96
|
+
status: 401,
|
|
97
|
+
statusText: 'Unauthorized',
|
|
98
|
+
text: async () => 'Invalid token',
|
|
99
|
+
};
|
|
100
|
+
(global.fetch as any).mockResolvedValueOnce(mockResponse);
|
|
101
|
+
|
|
102
|
+
const error = await handler.request('/test').catch((e) => e);
|
|
103
|
+
expect(error).toBeInstanceOf(AuthenticationError);
|
|
104
|
+
expect(error.message).toBe('Invalid Home Assistant token');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should throw ApiError on other HTTP errors', async () => {
|
|
108
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
109
|
+
ok: false,
|
|
110
|
+
status: 500,
|
|
111
|
+
statusText: 'Internal Server Error',
|
|
112
|
+
text: async () => 'Server error',
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await expect(handler.request('/test')).rejects.toThrow(ApiError);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should handle empty responses', async () => {
|
|
119
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
120
|
+
ok: true,
|
|
121
|
+
headers: new Map([['content-type', 'text/plain']]),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const result = await handler.request('/test');
|
|
125
|
+
|
|
126
|
+
expect(result).toEqual({});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should handle network errors', async () => {
|
|
130
|
+
(global.fetch as any).mockRejectedValueOnce(new Error('Network timeout'));
|
|
131
|
+
|
|
132
|
+
await expect(handler.request('/test')).rejects.toThrow('Network timeout');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('get', () => {
|
|
137
|
+
it('should make GET request', async () => {
|
|
138
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
139
|
+
ok: true,
|
|
140
|
+
headers: new Map([['content-type', 'application/json']]),
|
|
141
|
+
json: async () => ({ data: 'result' }),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const result = await handler.get<{ data: string }>('/states');
|
|
145
|
+
|
|
146
|
+
expect(result).toEqual({ data: 'result' });
|
|
147
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
148
|
+
expect.stringContaining('/states'),
|
|
149
|
+
expect.objectContaining({ method: 'GET' })
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should pass query parameters', async () => {
|
|
154
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
155
|
+
ok: true,
|
|
156
|
+
headers: new Map([['content-type', 'application/json']]),
|
|
157
|
+
json: async () => ({}),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await handler.get('/history', { entity_id: 'light.living_room' });
|
|
161
|
+
|
|
162
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
163
|
+
expect.stringContaining('entity_id=light.living_room'),
|
|
164
|
+
expect.any(Object)
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('post', () => {
|
|
170
|
+
it('should make POST request with body', async () => {
|
|
171
|
+
(global.fetch as any).mockResolvedValueOnce({
|
|
172
|
+
ok: true,
|
|
173
|
+
headers: new Map([['content-type', 'application/json']]),
|
|
174
|
+
json: async () => ({ context: { id: '123' } }),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const result = await handler.post('/services/light/turn_on', {
|
|
178
|
+
entity_id: 'light.living_room',
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(result).toEqual({ context: { id: '123' } });
|
|
182
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
183
|
+
expect.stringContaining('/services/light/turn_on'),
|
|
184
|
+
expect.objectContaining({
|
|
185
|
+
method: 'POST',
|
|
186
|
+
body: JSON.stringify({ entity_id: 'light.living_room' }),
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('SSL configuration', () => {
|
|
193
|
+
it('should create HTTPS agent when strictSsl is false', () => {
|
|
194
|
+
const handlerNoSsl = new RequestHandler({
|
|
195
|
+
baseUrl: 'https://homeassistant.10.0.0.19.nip.io:8123',
|
|
196
|
+
token: 'token',
|
|
197
|
+
timeout: 30000,
|
|
198
|
+
strictSsl: false,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Handler is created successfully with strictSsl: false
|
|
202
|
+
expect(handlerNoSsl).toBeDefined();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should not create HTTPS agent when strictSsl is true', () => {
|
|
206
|
+
const handlerWithSsl = new RequestHandler({
|
|
207
|
+
baseUrl: 'https://homeassistant.10.0.0.19.nip.io:8123',
|
|
208
|
+
token: 'token',
|
|
209
|
+
timeout: 30000,
|
|
210
|
+
strictSsl: true,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Handler is created successfully with strictSsl: true
|
|
214
|
+
expect(handlerWithSsl).toBeDefined();
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ServiceOperations tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
+
import { ServiceOperations } from '../../src/haClient/services.js';
|
|
7
|
+
import type { RequestHandler } from '../../src/haClient/request.js';
|
|
8
|
+
|
|
9
|
+
describe('ServiceOperations', () => {
|
|
10
|
+
let mockRequest: RequestHandler;
|
|
11
|
+
let serviceOps: ServiceOperations;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
mockRequest = {
|
|
15
|
+
post: vi.fn(),
|
|
16
|
+
} as unknown as RequestHandler;
|
|
17
|
+
|
|
18
|
+
serviceOps = new ServiceOperations(mockRequest);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('callService', () => {
|
|
22
|
+
it('should call service with domain and service name', async () => {
|
|
23
|
+
const mockResponse = {
|
|
24
|
+
context: { id: 'ctx-123', parent_id: null, user_id: 'user-456' },
|
|
25
|
+
};
|
|
26
|
+
(mockRequest.post as ReturnType<typeof vi.fn>).mockResolvedValue(mockResponse);
|
|
27
|
+
|
|
28
|
+
const result = await serviceOps.callService({
|
|
29
|
+
domain: 'light',
|
|
30
|
+
service: 'turn_on',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(result).toEqual(mockResponse);
|
|
34
|
+
expect(mockRequest.post).toHaveBeenCalledWith('/services/light/turn_on', {});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should include target entity_id', async () => {
|
|
38
|
+
const mockResponse = {
|
|
39
|
+
context: { id: 'ctx-123', parent_id: null, user_id: null },
|
|
40
|
+
};
|
|
41
|
+
(mockRequest.post as ReturnType<typeof vi.fn>).mockResolvedValue(mockResponse);
|
|
42
|
+
|
|
43
|
+
await serviceOps.callService({
|
|
44
|
+
domain: 'light',
|
|
45
|
+
service: 'turn_on',
|
|
46
|
+
target: { entity_id: 'light.living_room' },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(mockRequest.post).toHaveBeenCalledWith('/services/light/turn_on', {
|
|
50
|
+
entity_id: 'light.living_room',
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should include service_data', async () => {
|
|
55
|
+
const mockResponse = {
|
|
56
|
+
context: { id: 'ctx-123', parent_id: null, user_id: null },
|
|
57
|
+
};
|
|
58
|
+
(mockRequest.post as ReturnType<typeof vi.fn>).mockResolvedValue(mockResponse);
|
|
59
|
+
|
|
60
|
+
await serviceOps.callService({
|
|
61
|
+
domain: 'light',
|
|
62
|
+
service: 'turn_on',
|
|
63
|
+
target: { entity_id: 'light.living_room' },
|
|
64
|
+
service_data: { brightness: 255, color_temp: 370 },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(mockRequest.post).toHaveBeenCalledWith('/services/light/turn_on', {
|
|
68
|
+
entity_id: 'light.living_room',
|
|
69
|
+
brightness: 255,
|
|
70
|
+
color_temp: 370,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should handle multiple entity targets', async () => {
|
|
75
|
+
const mockResponse = {
|
|
76
|
+
context: { id: 'ctx-123', parent_id: null, user_id: null },
|
|
77
|
+
};
|
|
78
|
+
(mockRequest.post as ReturnType<typeof vi.fn>).mockResolvedValue(mockResponse);
|
|
79
|
+
|
|
80
|
+
await serviceOps.callService({
|
|
81
|
+
domain: 'light',
|
|
82
|
+
service: 'turn_off',
|
|
83
|
+
target: { entity_id: ['light.living_room', 'light.bedroom'] },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(mockRequest.post).toHaveBeenCalledWith('/services/light/turn_off', {
|
|
87
|
+
entity_id: ['light.living_room', 'light.bedroom'],
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should call climate service with temperature', async () => {
|
|
92
|
+
const mockResponse = {
|
|
93
|
+
context: { id: 'ctx-123', parent_id: null, user_id: null },
|
|
94
|
+
};
|
|
95
|
+
(mockRequest.post as ReturnType<typeof vi.fn>).mockResolvedValue(mockResponse);
|
|
96
|
+
|
|
97
|
+
await serviceOps.callService({
|
|
98
|
+
domain: 'climate',
|
|
99
|
+
service: 'set_temperature',
|
|
100
|
+
target: { entity_id: 'climate.living_room' },
|
|
101
|
+
service_data: { temperature: 22 },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(mockRequest.post).toHaveBeenCalledWith('/services/climate/set_temperature', {
|
|
105
|
+
entity_id: 'climate.living_room',
|
|
106
|
+
temperature: 22,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('restartServer', () => {
|
|
112
|
+
it('should call homeassistant restart service', async () => {
|
|
113
|
+
(mockRequest.post as ReturnType<typeof vi.fn>).mockResolvedValue({});
|
|
114
|
+
|
|
115
|
+
await serviceOps.restartServer();
|
|
116
|
+
|
|
117
|
+
expect(mockRequest.post).toHaveBeenCalledWith('/services/homeassistant/restart', {});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StateOperations tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
+
import { StateOperations } from '../../src/haClient/states.js';
|
|
7
|
+
import type { RequestHandler } from '../../src/haClient/request.js';
|
|
8
|
+
import type { Entity } from '../../src/types/index.js';
|
|
9
|
+
import { ApiError } from '../../src/types/index.js';
|
|
10
|
+
|
|
11
|
+
describe('StateOperations', () => {
|
|
12
|
+
let mockRequest: RequestHandler;
|
|
13
|
+
let stateOps: StateOperations;
|
|
14
|
+
|
|
15
|
+
const mockEntities: Entity[] = [
|
|
16
|
+
{
|
|
17
|
+
entity_id: 'light.living_room',
|
|
18
|
+
state: 'on',
|
|
19
|
+
attributes: { friendly_name: 'Living Room Light', brightness: 255 },
|
|
20
|
+
last_changed: '2026-01-08T10:00:00.000Z',
|
|
21
|
+
last_updated: '2026-01-08T10:00:00.000Z',
|
|
22
|
+
context: { id: 'ctx-1', parent_id: null, user_id: null },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
entity_id: 'sensor.temperature',
|
|
26
|
+
state: '22.5',
|
|
27
|
+
attributes: { friendly_name: 'Temperature', unit_of_measurement: '°C' },
|
|
28
|
+
last_changed: '2026-01-08T10:00:00.000Z',
|
|
29
|
+
last_updated: '2026-01-08T10:00:00.000Z',
|
|
30
|
+
context: { id: 'ctx-2', parent_id: null, user_id: null },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
entity_id: 'binary_sensor.motion',
|
|
34
|
+
state: 'off',
|
|
35
|
+
attributes: { friendly_name: 'Motion Sensor', device_class: 'motion' },
|
|
36
|
+
last_changed: '2026-01-08T09:00:00.000Z',
|
|
37
|
+
last_updated: '2026-01-08T10:00:00.000Z',
|
|
38
|
+
context: { id: 'ctx-3', parent_id: null, user_id: null },
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
mockRequest = {
|
|
44
|
+
get: vi.fn(),
|
|
45
|
+
} as unknown as RequestHandler;
|
|
46
|
+
|
|
47
|
+
stateOps = new StateOperations(mockRequest);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('getStates', () => {
|
|
51
|
+
it('should fetch all states', async () => {
|
|
52
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockResolvedValue(mockEntities);
|
|
53
|
+
|
|
54
|
+
const states = await stateOps.getStates();
|
|
55
|
+
|
|
56
|
+
expect(states).toEqual(mockEntities);
|
|
57
|
+
expect(mockRequest.get).toHaveBeenCalledWith('/states');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should return empty array when no entities exist', async () => {
|
|
61
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
|
62
|
+
|
|
63
|
+
const states = await stateOps.getStates();
|
|
64
|
+
|
|
65
|
+
expect(states).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('getState', () => {
|
|
70
|
+
it('should fetch specific entity state', async () => {
|
|
71
|
+
const entity = mockEntities[0];
|
|
72
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockResolvedValue(entity);
|
|
73
|
+
|
|
74
|
+
const state = await stateOps.getState('light.living_room');
|
|
75
|
+
|
|
76
|
+
expect(state).toEqual(entity);
|
|
77
|
+
expect(mockRequest.get).toHaveBeenCalledWith('/states/light.living_room');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should return null when entity not found', async () => {
|
|
81
|
+
const error = new ApiError('Not found', 404, { path: '/states/nonexistent' });
|
|
82
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockRejectedValue(error);
|
|
83
|
+
|
|
84
|
+
const state = await stateOps.getState('nonexistent');
|
|
85
|
+
|
|
86
|
+
expect(state).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should throw error for non-404 errors', async () => {
|
|
90
|
+
const error = new ApiError('Server error', 500, { path: '/states/test' });
|
|
91
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockRejectedValue(error);
|
|
92
|
+
|
|
93
|
+
await expect(stateOps.getState('test')).rejects.toThrow(ApiError);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('getAllSensors', () => {
|
|
98
|
+
it('should return only sensor and binary_sensor entities', async () => {
|
|
99
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockResolvedValue(mockEntities);
|
|
100
|
+
|
|
101
|
+
const sensors = await stateOps.getAllSensors();
|
|
102
|
+
|
|
103
|
+
expect(sensors).toHaveLength(2);
|
|
104
|
+
expect(sensors.map(s => s.entity_id)).toContain('sensor.temperature');
|
|
105
|
+
expect(sensors.map(s => s.entity_id)).toContain('binary_sensor.motion');
|
|
106
|
+
expect(sensors.map(s => s.entity_id)).not.toContain('light.living_room');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should return empty array when no sensors exist', async () => {
|
|
110
|
+
(mockRequest.get as ReturnType<typeof vi.fn>).mockResolvedValue([
|
|
111
|
+
{ ...mockEntities[0] }, // Only light entity
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
const sensors = await stateOps.getAllSensors();
|
|
115
|
+
|
|
116
|
+
expect(sensors).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|