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,492 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import type { ThinqAirConditionerDevice, ThinqWasherDevice } from '../../../core/domain/entities/ThinqDevice.js';
4
+ import { DEFAULT_AIR_CONDITIONER_CAPABILITIES } from '../../../core/domain/value-objects/AirConditionerCapabilities.js';
5
+ import { ThinqSnapshot } from '../../../core/domain/value-objects/ThinqSnapshot.js';
6
+ import type { PlatformConfigManager } from '../../../platform/platformConfigManager.js';
7
+ import { registerAirConditionerCommandHandlers } from '../../../platform/thinq/thinqAirConditionerCommandHandlers.js';
8
+ import { buildAirConditionerEndpoint } from '../../../platform/thinq/thinqAirConditionerEndpointFactory.js';
9
+ import { ThinqDeviceConfigurator } from '../../../platform/thinq/thinqDeviceConfigurator.js';
10
+ import { registerWasherCommandHandlers } from '../../../platform/thinq/thinqWasherCommandHandlers.js';
11
+ import { buildWasherEndpoint } from '../../../platform/thinq/thinqWasherEndpointFactory.js';
12
+ import type { ThinqApiClient } from '../../../services/thinq/thinqApiClient.js';
13
+ import { asPartial, createMockLogger } from '../../helpers/testUtils.js';
14
+
15
+ vi.mock('../../../platform/thinq/thinqAirConditionerCommandHandlers.js');
16
+ vi.mock('../../../platform/thinq/thinqAirConditionerAuxiliaryToggles.js', () => ({
17
+ registerAuxiliaryToggleCommandHandlers: vi.fn(),
18
+ }));
19
+ vi.mock('../../../platform/thinq/thinqAirConditionerEndpointFactory.js', () => ({
20
+ buildAirConditionerEndpoint: vi.fn(() => ({
21
+ log: { debug: vi.fn(), info: vi.fn(), error: vi.fn() },
22
+ createDefaultTemperatureMeasurementClusterServer: vi.fn().mockReturnThis(),
23
+ addRequiredClusterServers: vi.fn().mockReturnThis(),
24
+ })),
25
+ }));
26
+ vi.mock('../../../platform/thinq/thinqWasherEndpointFactory.js', () => ({
27
+ buildWasherEndpoint: vi.fn(() => ({
28
+ log: { debug: vi.fn(), info: vi.fn(), error: vi.fn() },
29
+ })),
30
+ }));
31
+ vi.mock('../../../platform/thinq/thinqWasherCommandHandlers.js');
32
+ vi.mock('../../../platform/thinq/thinqWasherStopCommandResolver.js');
33
+ vi.mock('../../../platform/thinq/thinqAirConditionerFilterResetCommandHandler.js', () => ({
34
+ registerFilterResetCommandHandler: vi.fn(),
35
+ }));
36
+
37
+ function createMockThinqAirConditionerDevice(): ThinqAirConditionerDevice {
38
+ return asPartial<ThinqAirConditionerDevice>({
39
+ id: 'device-123',
40
+ name: 'Living Room AC',
41
+ type: 'AC',
42
+ modelName: 'ModelXYZ',
43
+ platformType: 'THINQ',
44
+ online: true,
45
+ snapshot: new ThinqSnapshot({
46
+ 'airState.operation': 1,
47
+ 'airState.tempState.current': 22,
48
+ 'airState.tempState.target': 24,
49
+ 'airState.opMode': 0,
50
+ 'airState.windStrength': 2,
51
+ }),
52
+ });
53
+ }
54
+
55
+ function createMockApiClient(): ThinqApiClient {
56
+ return asPartial<ThinqApiClient>({
57
+ sendCommand: vi.fn().mockResolvedValue(undefined),
58
+ });
59
+ }
60
+
61
+ function createMockWasherDevice(): ThinqWasherDevice {
62
+ return asPartial<ThinqWasherDevice>({
63
+ id: 'washer-456',
64
+ name: 'Laundry Room Washer',
65
+ type: 'WASHER',
66
+ modelName: 'VCDWL_QEUK',
67
+ platformType: 'THINQ',
68
+ online: true,
69
+ snapshot: new ThinqSnapshot({
70
+ 'washerDryer.state': 'RUNNING',
71
+ }),
72
+ modelJsonUri: 'https://example.com/model.json',
73
+ });
74
+ }
75
+
76
+ function createMockConfigManager(): PlatformConfigManager {
77
+ return asPartial<PlatformConfigManager>({
78
+ getDeviceCapabilities: vi.fn().mockReturnValue(DEFAULT_AIR_CONDITIONER_CAPABILITIES),
79
+ getWasherControlConfig: vi.fn().mockReturnValue({}),
80
+ getAcFilterControlConfig: vi.fn().mockReturnValue({}),
81
+ overrideMatterConfiguration: false,
82
+ matterOverrideSettings: {
83
+ matterVendorName: 'Matterbridge',
84
+ matterVendorId: 0xfff1,
85
+ },
86
+ getProductNameForDevice: vi.fn().mockReturnValue(undefined),
87
+ getProductIdForDevice: vi.fn().mockReturnValue(undefined),
88
+ });
89
+ }
90
+
91
+ describe('ThinqDeviceConfigurator', () => {
92
+ let mockLogger: ReturnType<typeof createMockLogger>;
93
+ let mockApiClient: ThinqApiClient;
94
+ let mockConfigManager: PlatformConfigManager;
95
+ let configurator: ThinqDeviceConfigurator;
96
+
97
+ beforeEach(() => {
98
+ vi.clearAllMocks();
99
+ mockLogger = createMockLogger();
100
+ mockApiClient = createMockApiClient();
101
+ mockConfigManager = createMockConfigManager();
102
+ configurator = new ThinqDeviceConfigurator(mockLogger, mockApiClient, mockConfigManager);
103
+ });
104
+
105
+ afterEach(() => {
106
+ vi.clearAllMocks();
107
+ });
108
+
109
+ describe('registerAirConditioner', () => {
110
+ it('should log device registration', async () => {
111
+ // Arrange
112
+ const device = createMockThinqAirConditionerDevice();
113
+
114
+ // Act
115
+ await configurator.registerAirConditioner(device);
116
+
117
+ // Assert
118
+ expect(mockLogger.info).toHaveBeenCalledWith(
119
+ expect.stringContaining('Registering ThinQ AirConditioner: Living Room AC (device-123)'),
120
+ );
121
+ });
122
+
123
+ it('should call getDeviceCapabilities with the device id', async () => {
124
+ // Arrange
125
+ const device = createMockThinqAirConditionerDevice();
126
+ const getCapabilitiesSpy = vi.mocked(mockConfigManager.getDeviceCapabilities);
127
+
128
+ // Act
129
+ await configurator.registerAirConditioner(device);
130
+
131
+ // Assert
132
+ expect(getCapabilitiesSpy).toHaveBeenCalledWith('device-123');
133
+ });
134
+
135
+ it('should call registerAirConditionerCommandHandlers before returning', async () => {
136
+ // Arrange
137
+ const device = createMockThinqAirConditionerDevice();
138
+ const registerHandlersSpy = vi.mocked(registerAirConditionerCommandHandlers);
139
+
140
+ // Act
141
+ await configurator.registerAirConditioner(device);
142
+
143
+ // Assert
144
+ expect(registerHandlersSpy).toHaveBeenCalled();
145
+ });
146
+
147
+ it('should return a promise that resolves to an endpoint', async () => {
148
+ // Arrange
149
+ const device = createMockThinqAirConditionerDevice();
150
+
151
+ // Act
152
+ const result = await configurator.registerAirConditioner(device);
153
+
154
+ // Assert
155
+ expect(result).toBeDefined();
156
+ });
157
+
158
+ it('should hardcode the endpoint mode to server', async () => {
159
+ // Arrange
160
+ const device = createMockThinqAirConditionerDevice();
161
+
162
+ // Act
163
+ const endpoint = await configurator.registerAirConditioner(device);
164
+
165
+ // Assert
166
+ // The AC is always exposed as its own standalone Matter node (server mode), not user-configurable.
167
+ expect(endpoint.mode).toBe('server');
168
+ });
169
+
170
+ it('should use device snapshot values for initial state when defined', async () => {
171
+ // Arrange
172
+ const device = createMockThinqAirConditionerDevice();
173
+
174
+ // Act
175
+ await configurator.registerAirConditioner(device);
176
+
177
+ // Assert
178
+ // Verify it was called with the correct temperature from the device snapshot (22°C current, 24°C target)
179
+ expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('Living Room AC'));
180
+ });
181
+
182
+ it('should use default temperature when snapshot values are missing', async () => {
183
+ // Arrange
184
+ const deviceWithNoTemp = createMockThinqAirConditionerDevice();
185
+ // Create a new device with missing temperature data
186
+ const deviceWithoutTemp: ThinqAirConditionerDevice = asPartial<ThinqAirConditionerDevice>({
187
+ ...deviceWithNoTemp,
188
+ snapshot: new ThinqSnapshot({
189
+ 'airState.operation': 1,
190
+ 'airState.opMode': 0,
191
+ 'airState.windStrength': 2,
192
+ }),
193
+ });
194
+
195
+ // Act
196
+ await configurator.registerAirConditioner(deviceWithoutTemp);
197
+
198
+ // Assert
199
+ expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('Registering ThinQ AirConditioner'));
200
+ });
201
+
202
+ it('should call registerAirConditionerCommandHandlers with correct parameters', async () => {
203
+ // Arrange
204
+ const device = createMockThinqAirConditionerDevice();
205
+ const registerHandlersSpy = vi.mocked(registerAirConditionerCommandHandlers);
206
+
207
+ // Act
208
+ await configurator.registerAirConditioner(device);
209
+
210
+ // Assert
211
+ expect(registerHandlersSpy).toHaveBeenCalledWith(
212
+ expect.anything(), // endpoint
213
+ device,
214
+ mockApiClient,
215
+ mockLogger,
216
+ DEFAULT_AIR_CONDITIONER_CAPABILITIES,
217
+ );
218
+ });
219
+
220
+ it('should pass options with default product identity when overrideMatterConfiguration is false', async () => {
221
+ // Arrange
222
+ const device = createMockThinqAirConditionerDevice();
223
+ const buildEndpointSpy = vi.mocked(buildAirConditionerEndpoint);
224
+
225
+ // Act
226
+ await configurator.registerAirConditioner(device);
227
+
228
+ // Assert
229
+ expect(buildEndpointSpy).toHaveBeenCalledWith(
230
+ device,
231
+ expect.anything(),
232
+ expect.anything(),
233
+ expect.anything(),
234
+ expect.objectContaining({
235
+ vendorId: undefined,
236
+ vendorName: undefined,
237
+ productId: 0x8000,
238
+ productName: 'LG Air Conditioner',
239
+ }),
240
+ );
241
+ });
242
+
243
+ it('should pass options with matterOverrideSettings values when overrideMatterConfiguration is true and no per-device override', async () => {
244
+ // Arrange
245
+ const device = createMockThinqAirConditionerDevice();
246
+ const customSettings = {
247
+ matterVendorName: 'Custom Vendor',
248
+ matterVendorId: 0xabcd,
249
+ };
250
+ mockConfigManager = asPartial<PlatformConfigManager>({
251
+ getDeviceCapabilities: vi.fn().mockReturnValue(DEFAULT_AIR_CONDITIONER_CAPABILITIES),
252
+ getAcFilterControlConfig: vi.fn().mockReturnValue({}),
253
+ overrideMatterConfiguration: true,
254
+ matterOverrideSettings: customSettings,
255
+ getProductNameForDevice: vi.fn().mockReturnValue(undefined),
256
+ getProductIdForDevice: vi.fn().mockReturnValue(undefined),
257
+ });
258
+ configurator = new ThinqDeviceConfigurator(mockLogger, mockApiClient, mockConfigManager);
259
+ const buildEndpointSpy = vi.mocked(buildAirConditionerEndpoint);
260
+
261
+ // Act
262
+ await configurator.registerAirConditioner(device);
263
+
264
+ // Assert
265
+ expect(buildEndpointSpy).toHaveBeenCalledWith(
266
+ device,
267
+ expect.anything(),
268
+ expect.anything(),
269
+ expect.anything(),
270
+ expect.objectContaining({
271
+ vendorId: 0xabcd,
272
+ vendorName: 'Custom Vendor',
273
+ }),
274
+ );
275
+ });
276
+
277
+ it('should use per-device productName when available and override enabled', async () => {
278
+ // Arrange
279
+ const device = createMockThinqAirConditionerDevice();
280
+ const customSettings = {
281
+ matterVendorName: 'Custom Vendor',
282
+ matterVendorId: 0xabcd,
283
+ };
284
+ mockConfigManager = asPartial<PlatformConfigManager>({
285
+ getDeviceCapabilities: vi.fn().mockReturnValue(DEFAULT_AIR_CONDITIONER_CAPABILITIES),
286
+ getAcFilterControlConfig: vi.fn().mockReturnValue({}),
287
+ overrideMatterConfiguration: true,
288
+ matterOverrideSettings: customSettings,
289
+ getProductNameForDevice: vi.fn().mockReturnValue('Device-Specific AC'),
290
+ getProductIdForDevice: vi.fn().mockReturnValue(0xef01),
291
+ });
292
+ configurator = new ThinqDeviceConfigurator(mockLogger, mockApiClient, mockConfigManager);
293
+ const buildEndpointSpy = vi.mocked(buildAirConditionerEndpoint);
294
+
295
+ // Act
296
+ await configurator.registerAirConditioner(device);
297
+
298
+ // Assert
299
+ expect(buildEndpointSpy).toHaveBeenCalledWith(
300
+ device,
301
+ expect.anything(),
302
+ expect.anything(),
303
+ expect.anything(),
304
+ expect.objectContaining({
305
+ vendorId: 0xabcd,
306
+ vendorName: 'Custom Vendor',
307
+ productId: 0xef01,
308
+ productName: 'Device-Specific AC', // Per-device override takes precedence
309
+ }),
310
+ );
311
+ });
312
+
313
+ it('should call getProductNameForDevice with correct device id', async () => {
314
+ // Arrange
315
+ const device = createMockThinqAirConditionerDevice();
316
+ const customSettings = {
317
+ matterVendorName: 'Matterbridge',
318
+ matterVendorId: 0xfff1,
319
+ };
320
+ mockConfigManager = asPartial<PlatformConfigManager>({
321
+ getDeviceCapabilities: vi.fn().mockReturnValue(DEFAULT_AIR_CONDITIONER_CAPABILITIES),
322
+ getAcFilterControlConfig: vi.fn().mockReturnValue({}),
323
+ overrideMatterConfiguration: true,
324
+ matterOverrideSettings: customSettings,
325
+ getProductNameForDevice: vi.fn().mockReturnValue(undefined),
326
+ getProductIdForDevice: vi.fn().mockReturnValue(undefined),
327
+ });
328
+ configurator = new ThinqDeviceConfigurator(mockLogger, mockApiClient, mockConfigManager);
329
+ const getProductNameSpy = vi.mocked(mockConfigManager.getProductNameForDevice);
330
+
331
+ // Act
332
+ await configurator.registerAirConditioner(device);
333
+
334
+ // Assert
335
+ expect(getProductNameSpy).toHaveBeenCalledWith('device-123');
336
+ });
337
+
338
+ it('should call getAcFilterControlConfig with device id', async () => {
339
+ // Arrange
340
+ const device = createMockThinqAirConditionerDevice();
341
+ const getFilterControlConfigSpy = vi.mocked(mockConfigManager.getAcFilterControlConfig);
342
+
343
+ // Act
344
+ await configurator.registerAirConditioner(device);
345
+
346
+ // Assert
347
+ expect(getFilterControlConfigSpy).toHaveBeenCalledWith('device-123');
348
+ });
349
+
350
+ it('should import and call registerFilterResetCommandHandler', async () => {
351
+ // Arrange
352
+ const device = createMockThinqAirConditionerDevice();
353
+ // Note: We use dynamic import to get the mocked version since it's mocked at the module level
354
+ const { registerFilterResetCommandHandler: mockedHandler } =
355
+ await import('../../../platform/thinq/thinqAirConditionerFilterResetCommandHandler.js');
356
+
357
+ // Act
358
+ await configurator.registerAirConditioner(device);
359
+
360
+ // Assert
361
+ expect(vi.mocked(mockedHandler)).toHaveBeenCalledOnce();
362
+ });
363
+
364
+ it('should call registerFilterResetCommandHandler with correct parameters', async () => {
365
+ // Arrange
366
+ const device = createMockThinqAirConditionerDevice();
367
+ const filterResetControl = { allowFilterReset: true };
368
+ vi.mocked(mockConfigManager.getAcFilterControlConfig).mockReturnValue(filterResetControl);
369
+
370
+ // Import the mocked handler
371
+ const { registerFilterResetCommandHandler: mockedHandler } =
372
+ await import('../../../platform/thinq/thinqAirConditionerFilterResetCommandHandler.js');
373
+
374
+ // Act
375
+ await configurator.registerAirConditioner(device);
376
+
377
+ // Assert
378
+ expect(vi.mocked(mockedHandler)).toHaveBeenCalledWith(
379
+ expect.anything(), // endpoint
380
+ device,
381
+ mockApiClient,
382
+ mockLogger,
383
+ DEFAULT_AIR_CONDITIONER_CAPABILITIES,
384
+ filterResetControl,
385
+ );
386
+ });
387
+ });
388
+
389
+ describe('registerWasher', () => {
390
+ it('should log device registration', async () => {
391
+ // Arrange
392
+ const device = createMockWasherDevice();
393
+
394
+ // Act
395
+ await configurator.registerWasher(device);
396
+
397
+ // Assert
398
+ expect(mockLogger.info).toHaveBeenCalledWith(
399
+ expect.stringContaining('Registering ThinQ Washer: Laundry Room Washer (washer-456)'),
400
+ );
401
+ });
402
+
403
+ it('should call buildWasherEndpoint with the device and options', async () => {
404
+ // Arrange
405
+ const device = createMockWasherDevice();
406
+ const buildEndpointSpy = vi.mocked(buildWasherEndpoint);
407
+
408
+ // Act
409
+ await configurator.registerWasher(device);
410
+
411
+ // Assert
412
+ expect(buildEndpointSpy).toHaveBeenCalledWith(
413
+ device,
414
+ expect.objectContaining({
415
+ vendorId: undefined,
416
+ vendorName: undefined,
417
+ productId: 0x8001,
418
+ productName: 'LG Washer',
419
+ }),
420
+ );
421
+ });
422
+
423
+ it('should call registerWasherCommandHandlers with correct parameters', async () => {
424
+ // Arrange
425
+ const device = createMockWasherDevice();
426
+ const registerHandlersSpy = vi.mocked(registerWasherCommandHandlers);
427
+
428
+ // Act
429
+ await configurator.registerWasher(device);
430
+
431
+ // Assert
432
+ expect(registerHandlersSpy).toHaveBeenCalled();
433
+ const call = registerHandlersSpy.mock.calls[0];
434
+ expect(call[1]).toBe(device); // device parameter
435
+ expect(call[2]).toBe(mockApiClient); // apiClient parameter
436
+ expect(call[3]).toBe(mockLogger); // logger parameter
437
+ });
438
+
439
+ it('should return an endpoint', async () => {
440
+ // Arrange
441
+ const device = createMockWasherDevice();
442
+
443
+ // Act
444
+ const result = await configurator.registerWasher(device);
445
+
446
+ // Assert
447
+ expect(result).toBeDefined();
448
+ });
449
+
450
+ it('should call getWasherControlConfig with device id', async () => {
451
+ // Arrange
452
+ const device = createMockWasherDevice();
453
+ const getConfigSpy = vi.mocked(mockConfigManager.getWasherControlConfig);
454
+
455
+ // Act
456
+ await configurator.registerWasher(device);
457
+
458
+ // Assert
459
+ expect(getConfigSpy).toHaveBeenCalledWith('washer-456');
460
+ });
461
+
462
+ it('should pass washerControl config to registerWasherCommandHandlers', async () => {
463
+ // Arrange
464
+ const device = createMockWasherDevice();
465
+ const washerControl = { allowRemoteStop: true };
466
+ vi.mocked(mockConfigManager.getWasherControlConfig).mockReturnValue(washerControl);
467
+ const registerHandlersSpy = vi.mocked(registerWasherCommandHandlers);
468
+
469
+ // Act
470
+ await configurator.registerWasher(device);
471
+
472
+ // Assert
473
+ const call = registerHandlersSpy.mock.calls[0];
474
+ expect(call[4]).toEqual(washerControl); // washerControl parameter
475
+ });
476
+
477
+ it('should pass 5+ parameters to registerWasherCommandHandlers (includes stop payload)', async () => {
478
+ // Arrange
479
+ const device = createMockWasherDevice();
480
+ const registerHandlersSpy = vi.mocked(registerWasherCommandHandlers);
481
+
482
+ // Act
483
+ await configurator.registerWasher(device);
484
+
485
+ // Assert
486
+ const call = registerHandlersSpy.mock.calls[0];
487
+ // Verify the function is called with 6 parameters:
488
+ // washer, device, apiClient, logger, washerControl, stopCommandPayload
489
+ expect(call.length).toBe(6);
490
+ });
491
+ });
492
+ });