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.
Files changed (210) hide show
  1. package/.claude/CLAUDE.md +43 -0
  2. package/.claude/agents/compiler.md +91 -0
  3. package/.claude/agents/direct-executor.md +98 -0
  4. package/.claude/agents/documenter.md +78 -0
  5. package/.claude/agents/finalizer.md +233 -0
  6. package/.claude/agents/implementer.md +112 -0
  7. package/.claude/agents/investigator.md +100 -0
  8. package/.claude/agents/release-manager.md +203 -0
  9. package/.claude/agents/reviewer.md +131 -0
  10. package/.claude/agents/technical-architect.md +403 -0
  11. package/.claude/agents/test-writer.md +159 -0
  12. package/.claude/agents/wiki-manager.md +147 -0
  13. package/.claude/instructions/agent-prompts.md +90 -0
  14. package/.claude/instructions/shared-rules.md +11 -0
  15. package/.claude/instructions/team-orchestrator-policy.md +165 -0
  16. package/.claude/memory.md +186 -0
  17. package/.claude/output-styles/engineer-manager.md +38 -0
  18. package/.claude/settings.json +40 -0
  19. package/.claude/settings.local.json +30 -0
  20. package/.claude/skills/load-policy/SKILL.md +13 -0
  21. package/.claude/skills/ref-idea/SKILL.md +44 -0
  22. package/.claude/skills/status-of/SKILL.md +38 -0
  23. package/.claude/skills/workspace-console/SKILL.md +49 -0
  24. package/.claude/statusline-command.sh +130 -0
  25. package/.claude/statusline.md +53 -0
  26. package/.claude/templates/approval-packet.template.html +171 -0
  27. package/.claude/templates/reference-workspaces.md +16 -0
  28. package/.claude/tools/cli-runner-server.cjs +220 -0
  29. package/.claude/tools/discord-send-server.cjs +188 -0
  30. package/.claude/tools/github-release-server.cjs +181 -0
  31. package/.claude/tools/glob-grep-server.cjs +267 -0
  32. package/.claude/tools/read-log-server.cjs +175 -0
  33. package/.github/codeql/codeql-config.yml +14 -0
  34. package/.github/workflows/build.yml +54 -0
  35. package/.github/workflows/codeql.yml +59 -0
  36. package/.github/workflows/coverage.yml +70 -0
  37. package/.github/workflows/publish.yml +103 -0
  38. package/.mcp.json +34 -0
  39. package/CHANGELOG.md +195 -0
  40. package/LICENSE +21 -0
  41. package/dist/cli/commands/devices.js +52 -0
  42. package/dist/cli/commands/energy.js +122 -0
  43. package/dist/cli/commands/login.js +54 -0
  44. package/dist/cli/help.js +50 -0
  45. package/dist/cli/main.js +56 -0
  46. package/dist/cli/session.js +13 -0
  47. package/dist/cli/types.js +1 -0
  48. package/dist/cli/utils.js +29 -0
  49. package/dist/cli.js +2 -0
  50. package/dist/constants/index.js +1 -0
  51. package/dist/core/domain/entities/ThinqDevice.js +43 -0
  52. package/dist/core/domain/value-objects/AirConditionerCapabilities.js +22 -0
  53. package/dist/core/domain/value-objects/ThinqFilterState.js +35 -0
  54. package/dist/core/domain/value-objects/ThinqSnapshot.js +125 -0
  55. package/dist/errors/AuthenticationError.js +26 -0
  56. package/dist/errors/BaseError.js +23 -0
  57. package/dist/errors/index.js +2 -0
  58. package/dist/model/LgThinkqPluginPlatformConfig.js +23 -0
  59. package/dist/model/LgThinqPluginPlatformConfig.js +23 -0
  60. package/dist/module.js +212 -0
  61. package/dist/platform/deviceRegistry.js +26 -0
  62. package/dist/platform/platformConfigManager.js +89 -0
  63. package/dist/platform/platformState.js +9 -0
  64. package/dist/platform/thinq/thinqAirConditionerCommandHandlers.js +190 -0
  65. package/dist/platform/thinq/thinqAirConditionerEndpointFactory.js +53 -0
  66. package/dist/platform/thinq/thinqAirConditionerFilterResetCommandHandler.js +54 -0
  67. package/dist/platform/thinq/thinqAirConditionerFilterStateSync.js +19 -0
  68. package/dist/platform/thinq/thinqAirConditionerStateSync.js +205 -0
  69. package/dist/platform/thinq/thinqDeviceConfigReconciler.js +26 -0
  70. package/dist/platform/thinq/thinqDeviceConfigurator.js +124 -0
  71. package/dist/platform/thinq/thinqDeviceDiscovery.js +33 -0
  72. package/dist/platform/thinq/thinqWasherCommandHandlers.js +54 -0
  73. package/dist/platform/thinq/thinqWasherEndpointFactory.js +6 -0
  74. package/dist/platform/thinq/thinqWasherStateSync.js +31 -0
  75. package/dist/platform/thinq/thinqWasherStopCommandResolver.js +38 -0
  76. package/dist/services/authentication/AccountAuthStrategy.js +135 -0
  77. package/dist/services/authentication/AuthContext.js +1 -0
  78. package/dist/services/authentication/AuthenticationCoordinator.js +29 -0
  79. package/dist/services/authentication/IAuthStrategy.js +1 -0
  80. package/dist/services/authentication/TokenAuthStrategy.js +27 -0
  81. package/dist/services/authentication/UserDataRepository.js +32 -0
  82. package/dist/services/thinq/gateway.js +61 -0
  83. package/dist/services/thinq/mqtt/mqttCertificate.js +63 -0
  84. package/dist/services/thinq/mqtt/mqttConnectionEvents.js +37 -0
  85. package/dist/services/thinq/mqtt/mqttKeyRepository.js +33 -0
  86. package/dist/services/thinq/mqtt/mqttRetry.js +25 -0
  87. package/dist/services/thinq/mqtt/thinqMqttListener.js +97 -0
  88. package/dist/services/thinq/serviceContainer.js +73 -0
  89. package/dist/services/thinq/session.js +48 -0
  90. package/dist/services/thinq/thinqApiClient.js +258 -0
  91. package/dist/services/thinq/thinqDeviceService.js +75 -0
  92. package/dist/services/thinq/thinqFilterMonitoringService.js +40 -0
  93. package/dist/settings.js +9 -0
  94. package/dist/tests/helpers/testUtils.js +87 -0
  95. package/dist/utils/mask.js +39 -0
  96. package/docs/README_CLI.md +166 -0
  97. package/eslint.config.js +194 -0
  98. package/matterbridge-lg-thinq-plugin.config.json +27 -0
  99. package/matterbridge-lg-thinq-plugin.schema.json +520 -0
  100. package/package.json +107 -0
  101. package/prettier.config.js +49 -0
  102. package/scripts/parse-junit-failures.mjs +45 -0
  103. package/scripts/parse-tsc-failures.mjs +48 -0
  104. package/scripts/run-build-local-summary.mjs +29 -0
  105. package/scripts/run-format-summary.mjs +36 -0
  106. package/scripts/run-lint-fix-summary.mjs +38 -0
  107. package/scripts/run-precommit-summary.mjs +94 -0
  108. package/src/cli/commands/devices.ts +67 -0
  109. package/src/cli/commands/energy.ts +160 -0
  110. package/src/cli/commands/login.ts +74 -0
  111. package/src/cli/help.ts +53 -0
  112. package/src/cli/main.ts +66 -0
  113. package/src/cli/session.ts +15 -0
  114. package/src/cli/types.ts +10 -0
  115. package/src/cli/utils.ts +30 -0
  116. package/src/cli.ts +3 -0
  117. package/src/constants/index.ts +8 -0
  118. package/src/core/domain/entities/ThinqDevice.test.ts +224 -0
  119. package/src/core/domain/entities/ThinqDevice.ts +74 -0
  120. package/src/core/domain/value-objects/AirConditionerCapabilities.ts +50 -0
  121. package/src/core/domain/value-objects/ThinqFilterState.ts +50 -0
  122. package/src/core/domain/value-objects/ThinqSnapshot.ts +162 -0
  123. package/src/errors/AuthenticationError.ts +37 -0
  124. package/src/errors/BaseError.ts +38 -0
  125. package/src/errors/index.ts +24 -0
  126. package/src/model/LgThinqPluginPlatformConfig.ts +94 -0
  127. package/src/module.ts +298 -0
  128. package/src/platform/deviceRegistry.ts +58 -0
  129. package/src/platform/platformConfigManager.ts +133 -0
  130. package/src/platform/platformState.ts +16 -0
  131. package/src/platform/thinq/thinqAirConditionerCommandHandlers.ts +291 -0
  132. package/src/platform/thinq/thinqAirConditionerEndpointFactory.ts +145 -0
  133. package/src/platform/thinq/thinqAirConditionerFilterResetCommandHandler.ts +96 -0
  134. package/src/platform/thinq/thinqAirConditionerFilterStateSync.ts +42 -0
  135. package/src/platform/thinq/thinqAirConditionerStateSync.ts +343 -0
  136. package/src/platform/thinq/thinqDeviceConfigReconciler.ts +42 -0
  137. package/src/platform/thinq/thinqDeviceConfigurator.test.ts +90 -0
  138. package/src/platform/thinq/thinqDeviceConfigurator.ts +191 -0
  139. package/src/platform/thinq/thinqDeviceDiscovery.test.ts +173 -0
  140. package/src/platform/thinq/thinqDeviceDiscovery.ts +51 -0
  141. package/src/platform/thinq/thinqWasherCommandHandlers.ts +120 -0
  142. package/src/platform/thinq/thinqWasherEndpointFactory.ts +37 -0
  143. package/src/platform/thinq/thinqWasherStateSync.ts +60 -0
  144. package/src/platform/thinq/thinqWasherStopCommandResolver.ts +62 -0
  145. package/src/services/authentication/AccountAuthStrategy.test.ts +358 -0
  146. package/src/services/authentication/AccountAuthStrategy.ts +209 -0
  147. package/src/services/authentication/AuthContext.ts +19 -0
  148. package/src/services/authentication/AuthenticationCoordinator.test.ts +217 -0
  149. package/src/services/authentication/AuthenticationCoordinator.ts +59 -0
  150. package/src/services/authentication/IAuthStrategy.ts +10 -0
  151. package/src/services/authentication/TokenAuthStrategy.test.ts +167 -0
  152. package/src/services/authentication/TokenAuthStrategy.ts +40 -0
  153. package/src/services/authentication/UserDataRepository.test.ts +149 -0
  154. package/src/services/authentication/UserDataRepository.ts +46 -0
  155. package/src/services/thinq/gateway.ts +86 -0
  156. package/src/services/thinq/mqtt/mqttCertificate.test.ts +344 -0
  157. package/src/services/thinq/mqtt/mqttCertificate.ts +102 -0
  158. package/src/services/thinq/mqtt/mqttConnectionEvents.test.ts +276 -0
  159. package/src/services/thinq/mqtt/mqttConnectionEvents.ts +76 -0
  160. package/src/services/thinq/mqtt/mqttKeyRepository.test.ts +164 -0
  161. package/src/services/thinq/mqtt/mqttKeyRepository.ts +43 -0
  162. package/src/services/thinq/mqtt/mqttRetry.test.ts +239 -0
  163. package/src/services/thinq/mqtt/mqttRetry.ts +43 -0
  164. package/src/services/thinq/mqtt/thinqMqttListener.test.ts +349 -0
  165. package/src/services/thinq/mqtt/thinqMqttListener.ts +120 -0
  166. package/src/services/thinq/serviceContainer.test.ts +203 -0
  167. package/src/services/thinq/serviceContainer.ts +105 -0
  168. package/src/services/thinq/session.test.ts +287 -0
  169. package/src/services/thinq/session.ts +69 -0
  170. package/src/services/thinq/thinqApiClient.test.ts +702 -0
  171. package/src/services/thinq/thinqApiClient.ts +418 -0
  172. package/src/services/thinq/thinqDeviceService.test.ts +415 -0
  173. package/src/services/thinq/thinqDeviceService.ts +101 -0
  174. package/src/services/thinq/thinqFilterMonitoringService.test.ts +264 -0
  175. package/src/services/thinq/thinqFilterMonitoringService.ts +53 -0
  176. package/src/settings.ts +18 -0
  177. package/src/tests/cli/commands/devices.test.ts +293 -0
  178. package/src/tests/cli/commands/login.test.ts +240 -0
  179. package/src/tests/cli/help.test.ts +46 -0
  180. package/src/tests/cli/main.test.ts +214 -0
  181. package/src/tests/cli/session.test.ts +198 -0
  182. package/src/tests/cli/utils.test.ts +194 -0
  183. package/src/tests/core/domain/value-objects/AirConditionerCapabilities.test.ts +685 -0
  184. package/src/tests/core/domain/value-objects/ThinqFilterState.test.ts +210 -0
  185. package/src/tests/core/domain/value-objects/ThinqSnapshot.test.ts +1035 -0
  186. package/src/tests/helpers/testUtils.ts +118 -0
  187. package/src/tests/model/LgThinqPluginPlatformConfig.test.ts +81 -0
  188. package/src/tests/platform/deviceRegistry.test.ts +177 -0
  189. package/src/tests/platform/platformConfigManager.test.ts +790 -0
  190. package/src/tests/platform/platformState.test.ts +41 -0
  191. package/src/tests/platform/thinq/thinqAirConditionerCommandHandlers.test.ts +879 -0
  192. package/src/tests/platform/thinq/thinqAirConditionerEndpointFactory.test.ts +993 -0
  193. package/src/tests/platform/thinq/thinqAirConditionerFilterResetCommandHandler.test.ts +508 -0
  194. package/src/tests/platform/thinq/thinqAirConditionerFilterStateSync.test.ts +212 -0
  195. package/src/tests/platform/thinq/thinqAirConditionerStateSync.test.ts +1586 -0
  196. package/src/tests/platform/thinq/thinqDeviceConfigReconciler.test.ts +269 -0
  197. package/src/tests/platform/thinq/thinqDeviceConfigurator.test.ts +492 -0
  198. package/src/tests/platform/thinq/thinqWasherCommandHandlers.test.ts +594 -0
  199. package/src/tests/platform/thinq/thinqWasherEndpointFactory.test.ts +84 -0
  200. package/src/tests/platform/thinq/thinqWasherStateSync.test.ts +373 -0
  201. package/src/tests/platform/thinq/thinqWasherStopCommandResolver.test.ts +392 -0
  202. package/src/tests/settings.test.ts +33 -0
  203. package/src/utils/mask.ts +50 -0
  204. package/tsconfig.base.json +25 -0
  205. package/tsconfig.json +9 -0
  206. package/tsconfig.production.json +13 -0
  207. package/tsconfig.vitest.json +5 -0
  208. package/vite.config.ts +43 -0
  209. package/wiki/memory-archive.md +12 -0
  210. 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
+ });