matterbridge-lg-thinq-plugin 0.1.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/.claude/CLAUDE.md +43 -0
- package/.claude/agents/compiler.md +91 -0
- package/.claude/agents/direct-executor.md +98 -0
- package/.claude/agents/documenter.md +78 -0
- package/.claude/agents/finalizer.md +233 -0
- package/.claude/agents/implementer.md +112 -0
- package/.claude/agents/investigator.md +100 -0
- package/.claude/agents/release-manager.md +203 -0
- package/.claude/agents/reviewer.md +131 -0
- package/.claude/agents/technical-architect.md +403 -0
- package/.claude/agents/test-writer.md +159 -0
- package/.claude/agents/wiki-manager.md +147 -0
- package/.claude/instructions/agent-prompts.md +90 -0
- package/.claude/instructions/shared-rules.md +11 -0
- package/.claude/instructions/team-orchestrator-policy.md +165 -0
- package/.claude/memory.md +186 -0
- package/.claude/output-styles/engineer-manager.md +38 -0
- package/.claude/settings.json +40 -0
- package/.claude/settings.local.json +30 -0
- package/.claude/skills/load-policy/SKILL.md +13 -0
- package/.claude/skills/ref-idea/SKILL.md +44 -0
- package/.claude/skills/status-of/SKILL.md +38 -0
- package/.claude/skills/workspace-console/SKILL.md +49 -0
- package/.claude/statusline-command.sh +130 -0
- package/.claude/statusline.md +53 -0
- package/.claude/templates/approval-packet.template.html +171 -0
- package/.claude/templates/reference-workspaces.md +16 -0
- package/.claude/tools/cli-runner-server.cjs +220 -0
- package/.claude/tools/discord-send-server.cjs +188 -0
- package/.claude/tools/github-release-server.cjs +181 -0
- package/.claude/tools/glob-grep-server.cjs +267 -0
- package/.claude/tools/read-log-server.cjs +175 -0
- package/.github/codeql/codeql-config.yml +14 -0
- package/.github/workflows/build.yml +54 -0
- package/.github/workflows/codeql.yml +59 -0
- package/.github/workflows/coverage.yml +70 -0
- package/.github/workflows/publish.yml +103 -0
- package/.mcp.json +34 -0
- package/CHANGELOG.md +195 -0
- package/LICENSE +21 -0
- package/dist/cli/commands/devices.js +52 -0
- package/dist/cli/commands/energy.js +122 -0
- package/dist/cli/commands/login.js +54 -0
- package/dist/cli/help.js +50 -0
- package/dist/cli/main.js +56 -0
- package/dist/cli/session.js +13 -0
- package/dist/cli/types.js +1 -0
- package/dist/cli/utils.js +29 -0
- package/dist/cli.js +2 -0
- package/dist/constants/index.js +1 -0
- package/dist/core/domain/entities/ThinqDevice.js +43 -0
- package/dist/core/domain/value-objects/AirConditionerCapabilities.js +22 -0
- package/dist/core/domain/value-objects/ThinqFilterState.js +35 -0
- package/dist/core/domain/value-objects/ThinqSnapshot.js +125 -0
- package/dist/errors/AuthenticationError.js +26 -0
- package/dist/errors/BaseError.js +23 -0
- package/dist/errors/index.js +2 -0
- package/dist/model/LgThinkqPluginPlatformConfig.js +23 -0
- package/dist/model/LgThinqPluginPlatformConfig.js +23 -0
- package/dist/module.js +212 -0
- package/dist/platform/deviceRegistry.js +26 -0
- package/dist/platform/platformConfigManager.js +89 -0
- package/dist/platform/platformState.js +9 -0
- package/dist/platform/thinq/thinqAirConditionerCommandHandlers.js +190 -0
- package/dist/platform/thinq/thinqAirConditionerEndpointFactory.js +53 -0
- package/dist/platform/thinq/thinqAirConditionerFilterResetCommandHandler.js +54 -0
- package/dist/platform/thinq/thinqAirConditionerFilterStateSync.js +19 -0
- package/dist/platform/thinq/thinqAirConditionerStateSync.js +205 -0
- package/dist/platform/thinq/thinqDeviceConfigReconciler.js +26 -0
- package/dist/platform/thinq/thinqDeviceConfigurator.js +124 -0
- package/dist/platform/thinq/thinqDeviceDiscovery.js +33 -0
- package/dist/platform/thinq/thinqWasherCommandHandlers.js +54 -0
- package/dist/platform/thinq/thinqWasherEndpointFactory.js +6 -0
- package/dist/platform/thinq/thinqWasherStateSync.js +31 -0
- package/dist/platform/thinq/thinqWasherStopCommandResolver.js +38 -0
- package/dist/services/authentication/AccountAuthStrategy.js +135 -0
- package/dist/services/authentication/AuthContext.js +1 -0
- package/dist/services/authentication/AuthenticationCoordinator.js +29 -0
- package/dist/services/authentication/IAuthStrategy.js +1 -0
- package/dist/services/authentication/TokenAuthStrategy.js +27 -0
- package/dist/services/authentication/UserDataRepository.js +32 -0
- package/dist/services/thinq/gateway.js +61 -0
- package/dist/services/thinq/mqtt/mqttCertificate.js +63 -0
- package/dist/services/thinq/mqtt/mqttConnectionEvents.js +37 -0
- package/dist/services/thinq/mqtt/mqttKeyRepository.js +33 -0
- package/dist/services/thinq/mqtt/mqttRetry.js +25 -0
- package/dist/services/thinq/mqtt/thinqMqttListener.js +97 -0
- package/dist/services/thinq/serviceContainer.js +73 -0
- package/dist/services/thinq/session.js +48 -0
- package/dist/services/thinq/thinqApiClient.js +258 -0
- package/dist/services/thinq/thinqDeviceService.js +75 -0
- package/dist/services/thinq/thinqFilterMonitoringService.js +40 -0
- package/dist/settings.js +9 -0
- package/dist/tests/helpers/testUtils.js +87 -0
- package/dist/utils/mask.js +39 -0
- package/docs/README_CLI.md +166 -0
- package/eslint.config.js +194 -0
- package/matterbridge-lg-thinq-plugin.config.json +27 -0
- package/matterbridge-lg-thinq-plugin.schema.json +520 -0
- package/package.json +107 -0
- package/prettier.config.js +49 -0
- package/scripts/parse-junit-failures.mjs +45 -0
- package/scripts/parse-tsc-failures.mjs +48 -0
- package/scripts/run-build-local-summary.mjs +29 -0
- package/scripts/run-format-summary.mjs +36 -0
- package/scripts/run-lint-fix-summary.mjs +38 -0
- package/scripts/run-precommit-summary.mjs +94 -0
- package/src/cli/commands/devices.ts +67 -0
- package/src/cli/commands/energy.ts +160 -0
- package/src/cli/commands/login.ts +74 -0
- package/src/cli/help.ts +53 -0
- package/src/cli/main.ts +66 -0
- package/src/cli/session.ts +15 -0
- package/src/cli/types.ts +10 -0
- package/src/cli/utils.ts +30 -0
- package/src/cli.ts +3 -0
- package/src/constants/index.ts +8 -0
- package/src/core/domain/entities/ThinqDevice.test.ts +224 -0
- package/src/core/domain/entities/ThinqDevice.ts +74 -0
- package/src/core/domain/value-objects/AirConditionerCapabilities.ts +50 -0
- package/src/core/domain/value-objects/ThinqFilterState.ts +50 -0
- package/src/core/domain/value-objects/ThinqSnapshot.ts +162 -0
- package/src/errors/AuthenticationError.ts +37 -0
- package/src/errors/BaseError.ts +38 -0
- package/src/errors/index.ts +24 -0
- package/src/model/LgThinqPluginPlatformConfig.ts +94 -0
- package/src/module.ts +298 -0
- package/src/platform/deviceRegistry.ts +58 -0
- package/src/platform/platformConfigManager.ts +133 -0
- package/src/platform/platformState.ts +16 -0
- package/src/platform/thinq/thinqAirConditionerCommandHandlers.ts +291 -0
- package/src/platform/thinq/thinqAirConditionerEndpointFactory.ts +145 -0
- package/src/platform/thinq/thinqAirConditionerFilterResetCommandHandler.ts +96 -0
- package/src/platform/thinq/thinqAirConditionerFilterStateSync.ts +42 -0
- package/src/platform/thinq/thinqAirConditionerStateSync.ts +343 -0
- package/src/platform/thinq/thinqDeviceConfigReconciler.ts +42 -0
- package/src/platform/thinq/thinqDeviceConfigurator.test.ts +90 -0
- package/src/platform/thinq/thinqDeviceConfigurator.ts +191 -0
- package/src/platform/thinq/thinqDeviceDiscovery.test.ts +173 -0
- package/src/platform/thinq/thinqDeviceDiscovery.ts +51 -0
- package/src/platform/thinq/thinqWasherCommandHandlers.ts +120 -0
- package/src/platform/thinq/thinqWasherEndpointFactory.ts +37 -0
- package/src/platform/thinq/thinqWasherStateSync.ts +60 -0
- package/src/platform/thinq/thinqWasherStopCommandResolver.ts +62 -0
- package/src/services/authentication/AccountAuthStrategy.test.ts +358 -0
- package/src/services/authentication/AccountAuthStrategy.ts +209 -0
- package/src/services/authentication/AuthContext.ts +19 -0
- package/src/services/authentication/AuthenticationCoordinator.test.ts +217 -0
- package/src/services/authentication/AuthenticationCoordinator.ts +59 -0
- package/src/services/authentication/IAuthStrategy.ts +10 -0
- package/src/services/authentication/TokenAuthStrategy.test.ts +167 -0
- package/src/services/authentication/TokenAuthStrategy.ts +40 -0
- package/src/services/authentication/UserDataRepository.test.ts +149 -0
- package/src/services/authentication/UserDataRepository.ts +46 -0
- package/src/services/thinq/gateway.ts +86 -0
- package/src/services/thinq/mqtt/mqttCertificate.test.ts +344 -0
- package/src/services/thinq/mqtt/mqttCertificate.ts +102 -0
- package/src/services/thinq/mqtt/mqttConnectionEvents.test.ts +276 -0
- package/src/services/thinq/mqtt/mqttConnectionEvents.ts +76 -0
- package/src/services/thinq/mqtt/mqttKeyRepository.test.ts +164 -0
- package/src/services/thinq/mqtt/mqttKeyRepository.ts +43 -0
- package/src/services/thinq/mqtt/mqttRetry.test.ts +239 -0
- package/src/services/thinq/mqtt/mqttRetry.ts +43 -0
- package/src/services/thinq/mqtt/thinqMqttListener.test.ts +349 -0
- package/src/services/thinq/mqtt/thinqMqttListener.ts +120 -0
- package/src/services/thinq/serviceContainer.test.ts +203 -0
- package/src/services/thinq/serviceContainer.ts +105 -0
- package/src/services/thinq/session.test.ts +287 -0
- package/src/services/thinq/session.ts +69 -0
- package/src/services/thinq/thinqApiClient.test.ts +702 -0
- package/src/services/thinq/thinqApiClient.ts +418 -0
- package/src/services/thinq/thinqDeviceService.test.ts +415 -0
- package/src/services/thinq/thinqDeviceService.ts +101 -0
- package/src/services/thinq/thinqFilterMonitoringService.test.ts +264 -0
- package/src/services/thinq/thinqFilterMonitoringService.ts +53 -0
- package/src/settings.ts +18 -0
- package/src/tests/cli/commands/devices.test.ts +293 -0
- package/src/tests/cli/commands/login.test.ts +240 -0
- package/src/tests/cli/help.test.ts +46 -0
- package/src/tests/cli/main.test.ts +214 -0
- package/src/tests/cli/session.test.ts +198 -0
- package/src/tests/cli/utils.test.ts +194 -0
- package/src/tests/core/domain/value-objects/AirConditionerCapabilities.test.ts +685 -0
- package/src/tests/core/domain/value-objects/ThinqFilterState.test.ts +210 -0
- package/src/tests/core/domain/value-objects/ThinqSnapshot.test.ts +1035 -0
- package/src/tests/helpers/testUtils.ts +118 -0
- package/src/tests/model/LgThinqPluginPlatformConfig.test.ts +81 -0
- package/src/tests/platform/deviceRegistry.test.ts +177 -0
- package/src/tests/platform/platformConfigManager.test.ts +790 -0
- package/src/tests/platform/platformState.test.ts +41 -0
- package/src/tests/platform/thinq/thinqAirConditionerCommandHandlers.test.ts +879 -0
- package/src/tests/platform/thinq/thinqAirConditionerEndpointFactory.test.ts +993 -0
- package/src/tests/platform/thinq/thinqAirConditionerFilterResetCommandHandler.test.ts +508 -0
- package/src/tests/platform/thinq/thinqAirConditionerFilterStateSync.test.ts +212 -0
- package/src/tests/platform/thinq/thinqAirConditionerStateSync.test.ts +1586 -0
- package/src/tests/platform/thinq/thinqDeviceConfigReconciler.test.ts +269 -0
- package/src/tests/platform/thinq/thinqDeviceConfigurator.test.ts +492 -0
- package/src/tests/platform/thinq/thinqWasherCommandHandlers.test.ts +594 -0
- package/src/tests/platform/thinq/thinqWasherEndpointFactory.test.ts +84 -0
- package/src/tests/platform/thinq/thinqWasherStateSync.test.ts +373 -0
- package/src/tests/platform/thinq/thinqWasherStopCommandResolver.test.ts +392 -0
- package/src/tests/settings.test.ts +33 -0
- package/src/utils/mask.ts +50 -0
- package/tsconfig.base.json +25 -0
- package/tsconfig.json +9 -0
- package/tsconfig.production.json +13 -0
- package/tsconfig.vitest.json +5 -0
- package/vite.config.ts +43 -0
- package/wiki/memory-archive.md +12 -0
- package/wiki/reference-workspaces.md +23 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { ThinqAirConditionerDevice } from '../../../core/domain/entities/ThinqDevice.js';
|
|
4
|
+
import { DEFAULT_AIR_CONDITIONER_CAPABILITIES } from '../../../core/domain/value-objects/AirConditionerCapabilities.js';
|
|
5
|
+
import { ThinqFilterState } from '../../../core/domain/value-objects/ThinqFilterState.js';
|
|
6
|
+
import {
|
|
7
|
+
registerFilterResetCommandHandler,
|
|
8
|
+
resolveFilterResetPayload,
|
|
9
|
+
} from '../../../platform/thinq/thinqAirConditionerFilterResetCommandHandler.js';
|
|
10
|
+
import type { ThinqApiClient } from '../../../services/thinq/thinqApiClient.js';
|
|
11
|
+
import { asPartial, createMockLogger } from '../../helpers/testUtils.js';
|
|
12
|
+
|
|
13
|
+
function createMockApiClient(): ThinqApiClient {
|
|
14
|
+
return asPartial<ThinqApiClient>({
|
|
15
|
+
getFilterState: vi.fn(),
|
|
16
|
+
sendCommand: vi.fn(),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createMockAirConditionerDevice(): ThinqAirConditionerDevice {
|
|
21
|
+
return asPartial<ThinqAirConditionerDevice>({
|
|
22
|
+
id: 'device-123',
|
|
23
|
+
name: 'Living Room AC',
|
|
24
|
+
type: 'AC',
|
|
25
|
+
modelName: 'ModelXYZ',
|
|
26
|
+
platformType: 'THINQ',
|
|
27
|
+
online: true,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createMockEndpoint() {
|
|
32
|
+
return asPartial({
|
|
33
|
+
log: createMockLogger(),
|
|
34
|
+
addCommandHandler: vi.fn(),
|
|
35
|
+
updateAttribute: vi.fn(),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('thinqAirConditionerFilterResetCommandHandler', () => {
|
|
40
|
+
let mockLogger: ReturnType<typeof createMockLogger>;
|
|
41
|
+
let mockApiClient: ThinqApiClient;
|
|
42
|
+
let mockDevice: ThinqAirConditionerDevice;
|
|
43
|
+
let mockEndpoint: ReturnType<typeof createMockEndpoint>;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
vi.clearAllMocks();
|
|
47
|
+
mockLogger = createMockLogger();
|
|
48
|
+
mockApiClient = createMockApiClient();
|
|
49
|
+
mockDevice = createMockAirConditionerDevice();
|
|
50
|
+
mockEndpoint = createMockEndpoint();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
vi.clearAllMocks();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('resolveFilterResetPayload', () => {
|
|
58
|
+
it('should return exact payload for valid filterState with fixed date', () => {
|
|
59
|
+
// Arrange
|
|
60
|
+
const data = {
|
|
61
|
+
'airState.filterMngStates.useTime': 11,
|
|
62
|
+
'airState.filterMngStates.maxTime': 720,
|
|
63
|
+
};
|
|
64
|
+
const filterState = ThinqFilterState.fromRaw(data);
|
|
65
|
+
if (!filterState) throw new Error('filterState should be defined');
|
|
66
|
+
const now = new Date('2026-09-23T00:00:00Z');
|
|
67
|
+
|
|
68
|
+
// Act
|
|
69
|
+
const result = resolveFilterResetPayload(filterState, now);
|
|
70
|
+
|
|
71
|
+
// Assert
|
|
72
|
+
expect(result).toEqual({
|
|
73
|
+
'airState.filterMngState.useTime': 0,
|
|
74
|
+
'airState.filterMngState.remainTime': 720,
|
|
75
|
+
'airState.filterMngState.maxTime': 720,
|
|
76
|
+
'airState.filterMngState.changeDate': 20260923,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should have exactly 4 keys in the payload', () => {
|
|
81
|
+
// Arrange
|
|
82
|
+
const data = {
|
|
83
|
+
'airState.filterMngStates.useTime': 11,
|
|
84
|
+
'airState.filterMngStates.maxTime': 720,
|
|
85
|
+
};
|
|
86
|
+
const filterState = ThinqFilterState.fromRaw(data);
|
|
87
|
+
if (!filterState) throw new Error('filterState should be defined');
|
|
88
|
+
const now = new Date('2026-09-23T00:00:00Z');
|
|
89
|
+
|
|
90
|
+
// Act
|
|
91
|
+
const result = resolveFilterResetPayload(filterState, now);
|
|
92
|
+
|
|
93
|
+
// Assert
|
|
94
|
+
expect(Object.keys(result)).toHaveLength(4);
|
|
95
|
+
expect(Object.keys(result).sort()).toEqual([
|
|
96
|
+
'airState.filterMngState.changeDate',
|
|
97
|
+
'airState.filterMngState.maxTime',
|
|
98
|
+
'airState.filterMngState.remainTime',
|
|
99
|
+
'airState.filterMngState.useTime',
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should format changeDate with zero-padding for single-digit month/day', () => {
|
|
104
|
+
// Arrange
|
|
105
|
+
const data = {
|
|
106
|
+
'airState.filterMngStates.useTime': 11,
|
|
107
|
+
'airState.filterMngStates.maxTime': 720,
|
|
108
|
+
};
|
|
109
|
+
const filterState = ThinqFilterState.fromRaw(data);
|
|
110
|
+
if (!filterState) throw new Error('filterState should be defined');
|
|
111
|
+
const now = new Date('2026-01-05T00:00:00Z');
|
|
112
|
+
|
|
113
|
+
// Act
|
|
114
|
+
const result = resolveFilterResetPayload(filterState, now);
|
|
115
|
+
|
|
116
|
+
// Assert
|
|
117
|
+
expect(result['airState.filterMngState.changeDate']).toBe(20260105);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should use current date when now is omitted', () => {
|
|
121
|
+
// Arrange
|
|
122
|
+
const data = {
|
|
123
|
+
'airState.filterMngStates.useTime': 11,
|
|
124
|
+
'airState.filterMngStates.maxTime': 720,
|
|
125
|
+
};
|
|
126
|
+
const filterState = ThinqFilterState.fromRaw(data);
|
|
127
|
+
if (!filterState) throw new Error('filterState should be defined');
|
|
128
|
+
|
|
129
|
+
// Act
|
|
130
|
+
const result = resolveFilterResetPayload(filterState);
|
|
131
|
+
|
|
132
|
+
// Assert
|
|
133
|
+
expect(result['airState.filterMngState.changeDate']).toBeDefined();
|
|
134
|
+
expect(result['airState.filterMngState.changeDate']).toBeGreaterThan(20000000);
|
|
135
|
+
expect(result['airState.filterMngState.changeDate']).toBeLessThan(99999999);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should echo back ratedMaxTimeHours unchanged', () => {
|
|
139
|
+
// Arrange
|
|
140
|
+
const data = {
|
|
141
|
+
'airState.filterMngStates.useTime': 100,
|
|
142
|
+
'airState.filterMngStates.maxTime': 500,
|
|
143
|
+
};
|
|
144
|
+
const filterState = ThinqFilterState.fromRaw(data);
|
|
145
|
+
if (!filterState) throw new Error('filterState should be defined');
|
|
146
|
+
const now = new Date('2026-09-23T00:00:00Z');
|
|
147
|
+
|
|
148
|
+
// Act
|
|
149
|
+
const result = resolveFilterResetPayload(filterState, now);
|
|
150
|
+
|
|
151
|
+
// Assert
|
|
152
|
+
expect(result['airState.filterMngState.remainTime']).toBe(500);
|
|
153
|
+
expect(result['airState.filterMngState.maxTime']).toBe(500);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should reset useTime to 0', () => {
|
|
157
|
+
// Arrange
|
|
158
|
+
const data = {
|
|
159
|
+
'airState.filterMngStates.useTime': 500,
|
|
160
|
+
'airState.filterMngStates.maxTime': 720,
|
|
161
|
+
};
|
|
162
|
+
const filterState = ThinqFilterState.fromRaw(data);
|
|
163
|
+
if (!filterState) throw new Error('filterState should be defined');
|
|
164
|
+
const now = new Date('2026-09-23T00:00:00Z');
|
|
165
|
+
|
|
166
|
+
// Act
|
|
167
|
+
const result = resolveFilterResetPayload(filterState, now);
|
|
168
|
+
|
|
169
|
+
// Assert
|
|
170
|
+
expect(result['airState.filterMngState.useTime']).toBe(0);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('registerFilterResetCommandHandler', () => {
|
|
175
|
+
describe('registration gating', () => {
|
|
176
|
+
it('should not call addCommandHandler when supportsFilterMonitoring is false', () => {
|
|
177
|
+
// Arrange
|
|
178
|
+
const capabilities = { ...DEFAULT_AIR_CONDITIONER_CAPABILITIES, supportsFilterMonitoring: false };
|
|
179
|
+
|
|
180
|
+
// Act
|
|
181
|
+
registerFilterResetCommandHandler(mockEndpoint as any, mockDevice, mockApiClient, mockLogger, capabilities, {});
|
|
182
|
+
|
|
183
|
+
// Assert
|
|
184
|
+
expect(vi.mocked(mockEndpoint.addCommandHandler)).not.toHaveBeenCalled();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should call addCommandHandler when supportsFilterMonitoring is true', () => {
|
|
188
|
+
// Arrange
|
|
189
|
+
const capabilities = { ...DEFAULT_AIR_CONDITIONER_CAPABILITIES, supportsFilterMonitoring: true };
|
|
190
|
+
|
|
191
|
+
// Act
|
|
192
|
+
registerFilterResetCommandHandler(mockEndpoint as any, mockDevice, mockApiClient, mockLogger, capabilities, {});
|
|
193
|
+
|
|
194
|
+
// Assert
|
|
195
|
+
expect(vi.mocked(mockEndpoint.addCommandHandler)).toHaveBeenCalledExactlyOnceWith(
|
|
196
|
+
'resetCondition',
|
|
197
|
+
expect.any(Function),
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('handler function when allowFilterReset is false', () => {
|
|
203
|
+
it('should reject when allowFilterReset is false', async () => {
|
|
204
|
+
// Arrange
|
|
205
|
+
const capabilities = { ...DEFAULT_AIR_CONDITIONER_CAPABILITIES, supportsFilterMonitoring: true };
|
|
206
|
+
registerFilterResetCommandHandler(mockEndpoint as any, mockDevice, mockApiClient, mockLogger, capabilities, {
|
|
207
|
+
allowFilterReset: false,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const handlerCall = vi.mocked(mockEndpoint.addCommandHandler).mock.calls[0];
|
|
211
|
+
const handler = handlerCall[1] as () => Promise<void>;
|
|
212
|
+
|
|
213
|
+
// Act & Assert
|
|
214
|
+
await expect(handler()).rejects.toThrow(/disabled by configuration/);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should not call getFilterState when allowFilterReset is false', async () => {
|
|
218
|
+
// Arrange
|
|
219
|
+
const capabilities = { ...DEFAULT_AIR_CONDITIONER_CAPABILITIES, supportsFilterMonitoring: true };
|
|
220
|
+
registerFilterResetCommandHandler(mockEndpoint as any, mockDevice, mockApiClient, mockLogger, capabilities, {
|
|
221
|
+
allowFilterReset: false,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const handlerCall = vi.mocked(mockEndpoint.addCommandHandler).mock.calls[0];
|
|
225
|
+
const handler = handlerCall[1] as () => Promise<void>;
|
|
226
|
+
|
|
227
|
+
// Act
|
|
228
|
+
try {
|
|
229
|
+
await handler();
|
|
230
|
+
} catch {
|
|
231
|
+
// Expected to reject
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Assert
|
|
235
|
+
expect(vi.mocked(mockApiClient.getFilterState)).not.toHaveBeenCalled();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should not call sendCommand when allowFilterReset is false', async () => {
|
|
239
|
+
// Arrange
|
|
240
|
+
const capabilities = { ...DEFAULT_AIR_CONDITIONER_CAPABILITIES, supportsFilterMonitoring: true };
|
|
241
|
+
registerFilterResetCommandHandler(mockEndpoint as any, mockDevice, mockApiClient, mockLogger, capabilities, {
|
|
242
|
+
allowFilterReset: false,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const handlerCall = vi.mocked(mockEndpoint.addCommandHandler).mock.calls[0];
|
|
246
|
+
const handler = handlerCall[1] as () => Promise<void>;
|
|
247
|
+
|
|
248
|
+
// Act
|
|
249
|
+
try {
|
|
250
|
+
await handler();
|
|
251
|
+
} catch {
|
|
252
|
+
// Expected to reject
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Assert
|
|
256
|
+
expect(vi.mocked(mockApiClient.sendCommand)).not.toHaveBeenCalled();
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('handler function when getFilterState returns undefined', () => {
|
|
261
|
+
it('should reject when getFilterState parses to undefined (missing maxTime)', async () => {
|
|
262
|
+
// Arrange
|
|
263
|
+
const capabilities = { ...DEFAULT_AIR_CONDITIONER_CAPABILITIES, supportsFilterMonitoring: true };
|
|
264
|
+
vi.mocked(mockApiClient.getFilterState).mockResolvedValue({
|
|
265
|
+
'airState.filterMngStates.useTime': 11,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
registerFilterResetCommandHandler(mockEndpoint as any, mockDevice, mockApiClient, mockLogger, capabilities, {
|
|
269
|
+
allowFilterReset: true,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const handlerCall = vi.mocked(mockEndpoint.addCommandHandler).mock.calls[0];
|
|
273
|
+
const handler = handlerCall[1] as () => Promise<void>;
|
|
274
|
+
|
|
275
|
+
// Act & Assert
|
|
276
|
+
await expect(handler()).rejects.toThrow(/no current filter data available/);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should not call sendCommand when filter data is undefined', async () => {
|
|
280
|
+
// Arrange
|
|
281
|
+
const capabilities = { ...DEFAULT_AIR_CONDITIONER_CAPABILITIES, supportsFilterMonitoring: true };
|
|
282
|
+
vi.mocked(mockApiClient.getFilterState).mockResolvedValue({
|
|
283
|
+
'airState.filterMngStates.useTime': 11,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
registerFilterResetCommandHandler(mockEndpoint as any, mockDevice, mockApiClient, mockLogger, capabilities, {
|
|
287
|
+
allowFilterReset: true,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const handlerCall = vi.mocked(mockEndpoint.addCommandHandler).mock.calls[0];
|
|
291
|
+
const handler = handlerCall[1] as () => Promise<void>;
|
|
292
|
+
|
|
293
|
+
// Act
|
|
294
|
+
try {
|
|
295
|
+
await handler();
|
|
296
|
+
} catch {
|
|
297
|
+
// Expected to reject
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Assert
|
|
301
|
+
expect(vi.mocked(mockApiClient.sendCommand)).not.toHaveBeenCalled();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe('handler function when getFilterState rejects', () => {
|
|
306
|
+
it('should reject and propagate error when getFilterState throws', async () => {
|
|
307
|
+
// Arrange
|
|
308
|
+
const capabilities = { ...DEFAULT_AIR_CONDITIONER_CAPABILITIES, supportsFilterMonitoring: true };
|
|
309
|
+
const testError = new Error('Network error');
|
|
310
|
+
vi.mocked(mockApiClient.getFilterState).mockRejectedValue(testError);
|
|
311
|
+
|
|
312
|
+
registerFilterResetCommandHandler(mockEndpoint as any, mockDevice, mockApiClient, mockLogger, capabilities, {
|
|
313
|
+
allowFilterReset: true,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const handlerCall = vi.mocked(mockEndpoint.addCommandHandler).mock.calls[0];
|
|
317
|
+
const handler = handlerCall[1] as () => Promise<void>;
|
|
318
|
+
|
|
319
|
+
// Act & Assert
|
|
320
|
+
await expect(handler()).rejects.toBe(testError);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should not call sendCommand when getFilterState throws', async () => {
|
|
324
|
+
// Arrange
|
|
325
|
+
const capabilities = { ...DEFAULT_AIR_CONDITIONER_CAPABILITIES, supportsFilterMonitoring: true };
|
|
326
|
+
vi.mocked(mockApiClient.getFilterState).mockRejectedValue(new Error('Network error'));
|
|
327
|
+
|
|
328
|
+
registerFilterResetCommandHandler(mockEndpoint as any, mockDevice, mockApiClient, mockLogger, capabilities, {
|
|
329
|
+
allowFilterReset: true,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const handlerCall = vi.mocked(mockEndpoint.addCommandHandler).mock.calls[0];
|
|
333
|
+
const handler = handlerCall[1] as () => Promise<void>;
|
|
334
|
+
|
|
335
|
+
// Act
|
|
336
|
+
try {
|
|
337
|
+
await handler();
|
|
338
|
+
} catch {
|
|
339
|
+
// Expected to reject
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Assert
|
|
343
|
+
expect(vi.mocked(mockApiClient.sendCommand)).not.toHaveBeenCalled();
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe('handler function on success path', () => {
|
|
348
|
+
it('should call sendCommand with correct payload when filter data is valid', async () => {
|
|
349
|
+
// Arrange
|
|
350
|
+
const capabilities = { ...DEFAULT_AIR_CONDITIONER_CAPABILITIES, supportsFilterMonitoring: true };
|
|
351
|
+
const filterData = {
|
|
352
|
+
'airState.filterMngStates.useTime': 11,
|
|
353
|
+
'airState.filterMngStates.maxTime': 720,
|
|
354
|
+
};
|
|
355
|
+
vi.mocked(mockApiClient.getFilterState).mockResolvedValue(filterData);
|
|
356
|
+
vi.mocked(mockApiClient.sendCommand).mockResolvedValue(undefined);
|
|
357
|
+
|
|
358
|
+
registerFilterResetCommandHandler(mockEndpoint as any, mockDevice, mockApiClient, mockLogger, capabilities, {
|
|
359
|
+
allowFilterReset: true,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const handlerCall = vi.mocked(mockEndpoint.addCommandHandler).mock.calls[0];
|
|
363
|
+
const handler = handlerCall[1] as () => Promise<void>;
|
|
364
|
+
|
|
365
|
+
// Act
|
|
366
|
+
await handler();
|
|
367
|
+
|
|
368
|
+
// Assert
|
|
369
|
+
expect(vi.mocked(mockApiClient.sendCommand)).toHaveBeenCalledOnce();
|
|
370
|
+
const sendCommandCall = vi.mocked(mockApiClient.sendCommand).mock.calls[0];
|
|
371
|
+
expect(sendCommandCall[0]).toBe('device-123');
|
|
372
|
+
expect(sendCommandCall[1]).toMatchObject({
|
|
373
|
+
ctrlKey: 'filterMngStateCtrl',
|
|
374
|
+
command: 'Set',
|
|
375
|
+
});
|
|
376
|
+
expect(sendCommandCall[1].dataSetList).toBeDefined();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('should build dataSetList from resolveFilterResetPayload', async () => {
|
|
380
|
+
// Arrange
|
|
381
|
+
const capabilities = { ...DEFAULT_AIR_CONDITIONER_CAPABILITIES, supportsFilterMonitoring: true };
|
|
382
|
+
const filterData = {
|
|
383
|
+
'airState.filterMngStates.useTime': 11,
|
|
384
|
+
'airState.filterMngStates.maxTime': 720,
|
|
385
|
+
};
|
|
386
|
+
vi.mocked(mockApiClient.getFilterState).mockResolvedValue(filterData);
|
|
387
|
+
vi.mocked(mockApiClient.sendCommand).mockResolvedValue(undefined);
|
|
388
|
+
|
|
389
|
+
registerFilterResetCommandHandler(mockEndpoint as any, mockDevice, mockApiClient, mockLogger, capabilities, {
|
|
390
|
+
allowFilterReset: true,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const handlerCall = vi.mocked(mockEndpoint.addCommandHandler).mock.calls[0];
|
|
394
|
+
const handler = handlerCall[1] as () => Promise<void>;
|
|
395
|
+
|
|
396
|
+
// Act
|
|
397
|
+
await handler();
|
|
398
|
+
|
|
399
|
+
// Assert
|
|
400
|
+
const sendCommandCall = vi.mocked(mockApiClient.sendCommand).mock.calls[0];
|
|
401
|
+
const dataSetList = sendCommandCall[1].dataSetList as Record<string, unknown>;
|
|
402
|
+
expect(dataSetList['airState.filterMngState.useTime']).toBe(0);
|
|
403
|
+
expect(dataSetList['airState.filterMngState.remainTime']).toBe(720);
|
|
404
|
+
expect(dataSetList['airState.filterMngState.maxTime']).toBe(720);
|
|
405
|
+
expect(dataSetList['airState.filterMngState.changeDate']).toBeDefined();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('should resolve without throwing on success', async () => {
|
|
409
|
+
// Arrange
|
|
410
|
+
const capabilities = { ...DEFAULT_AIR_CONDITIONER_CAPABILITIES, supportsFilterMonitoring: true };
|
|
411
|
+
const filterData = {
|
|
412
|
+
'airState.filterMngStates.useTime': 11,
|
|
413
|
+
'airState.filterMngStates.maxTime': 720,
|
|
414
|
+
};
|
|
415
|
+
vi.mocked(mockApiClient.getFilterState).mockResolvedValue(filterData);
|
|
416
|
+
vi.mocked(mockApiClient.sendCommand).mockResolvedValue(undefined);
|
|
417
|
+
|
|
418
|
+
registerFilterResetCommandHandler(mockEndpoint as any, mockDevice, mockApiClient, mockLogger, capabilities, {
|
|
419
|
+
allowFilterReset: true,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const handlerCall = vi.mocked(mockEndpoint.addCommandHandler).mock.calls[0];
|
|
423
|
+
const handler = handlerCall[1] as () => Promise<void>;
|
|
424
|
+
|
|
425
|
+
// Act & Assert
|
|
426
|
+
await expect(handler()).resolves.toBeUndefined();
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
describe('handler function when sendCommand rejects', () => {
|
|
431
|
+
it('should reject and propagate error when sendCommand throws', async () => {
|
|
432
|
+
// Arrange
|
|
433
|
+
const capabilities = { ...DEFAULT_AIR_CONDITIONER_CAPABILITIES, supportsFilterMonitoring: true };
|
|
434
|
+
const filterData = {
|
|
435
|
+
'airState.filterMngStates.useTime': 11,
|
|
436
|
+
'airState.filterMngStates.maxTime': 720,
|
|
437
|
+
};
|
|
438
|
+
const testError = new Error('ThinQ API error');
|
|
439
|
+
vi.mocked(mockApiClient.getFilterState).mockResolvedValue(filterData);
|
|
440
|
+
vi.mocked(mockApiClient.sendCommand).mockRejectedValue(testError);
|
|
441
|
+
|
|
442
|
+
registerFilterResetCommandHandler(mockEndpoint as any, mockDevice, mockApiClient, mockLogger, capabilities, {
|
|
443
|
+
allowFilterReset: true,
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const handlerCall = vi.mocked(mockEndpoint.addCommandHandler).mock.calls[0];
|
|
447
|
+
const handler = handlerCall[1] as () => Promise<void>;
|
|
448
|
+
|
|
449
|
+
// Act & Assert
|
|
450
|
+
await expect(handler()).rejects.toBe(testError);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('should never call updateAttribute on failure', async () => {
|
|
454
|
+
// Arrange
|
|
455
|
+
const capabilities = { ...DEFAULT_AIR_CONDITIONER_CAPABILITIES, supportsFilterMonitoring: true };
|
|
456
|
+
const filterData = {
|
|
457
|
+
'airState.filterMngStates.useTime': 11,
|
|
458
|
+
'airState.filterMngStates.maxTime': 720,
|
|
459
|
+
};
|
|
460
|
+
vi.mocked(mockApiClient.getFilterState).mockResolvedValue(filterData);
|
|
461
|
+
vi.mocked(mockApiClient.sendCommand).mockRejectedValue(new Error('ThinQ API error'));
|
|
462
|
+
|
|
463
|
+
registerFilterResetCommandHandler(mockEndpoint as any, mockDevice, mockApiClient, mockLogger, capabilities, {
|
|
464
|
+
allowFilterReset: true,
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const handlerCall = vi.mocked(mockEndpoint.addCommandHandler).mock.calls[0];
|
|
468
|
+
const handler = handlerCall[1] as () => Promise<void>;
|
|
469
|
+
|
|
470
|
+
// Act
|
|
471
|
+
try {
|
|
472
|
+
await handler();
|
|
473
|
+
} catch {
|
|
474
|
+
// Expected to reject
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Assert
|
|
478
|
+
expect(vi.mocked(mockEndpoint.updateAttribute)).not.toHaveBeenCalled();
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
describe('handler function should never call updateAttribute', () => {
|
|
483
|
+
it('should never call updateAttribute even on success (SDK does it)', async () => {
|
|
484
|
+
// Arrange
|
|
485
|
+
const capabilities = { ...DEFAULT_AIR_CONDITIONER_CAPABILITIES, supportsFilterMonitoring: true };
|
|
486
|
+
const filterData = {
|
|
487
|
+
'airState.filterMngStates.useTime': 11,
|
|
488
|
+
'airState.filterMngStates.maxTime': 720,
|
|
489
|
+
};
|
|
490
|
+
vi.mocked(mockApiClient.getFilterState).mockResolvedValue(filterData);
|
|
491
|
+
vi.mocked(mockApiClient.sendCommand).mockResolvedValue(undefined);
|
|
492
|
+
|
|
493
|
+
registerFilterResetCommandHandler(mockEndpoint as any, mockDevice, mockApiClient, mockLogger, capabilities, {
|
|
494
|
+
allowFilterReset: true,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const handlerCall = vi.mocked(mockEndpoint.addCommandHandler).mock.calls[0];
|
|
498
|
+
const handler = handlerCall[1] as () => Promise<void>;
|
|
499
|
+
|
|
500
|
+
// Act
|
|
501
|
+
await handler();
|
|
502
|
+
|
|
503
|
+
// Assert
|
|
504
|
+
expect(vi.mocked(mockEndpoint.updateAttribute)).not.toHaveBeenCalled();
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
});
|