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,392 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ extractWasherStopCommand,
5
+ type WasherStopCommandPayload,
6
+ } from '../../../platform/thinq/thinqWasherStopCommandResolver.js';
7
+
8
+ describe('thinqWasherStopCommandResolver', () => {
9
+ describe('extractWasherStopCommand', () => {
10
+ it('should extract WMStop command when present', () => {
11
+ // Arrange
12
+ const deviceModel = {
13
+ ControlWifi: {
14
+ WMStop: {
15
+ command: 'Set',
16
+ dataKey: 'washerDryer.state',
17
+ dataValue: 'STOP',
18
+ },
19
+ },
20
+ };
21
+
22
+ // Act
23
+ const result = extractWasherStopCommand(deviceModel);
24
+
25
+ // Assert
26
+ expect(result).toBeDefined();
27
+ expect(result?.command).toBe('Set');
28
+ expect(result?.dataKey).toBe('washerDryer.state');
29
+ expect(result?.dataValue).toBe('STOP');
30
+ });
31
+
32
+ it('should fall back to WMOff when WMStop is absent', () => {
33
+ // Arrange
34
+ const deviceModel = {
35
+ ControlWifi: {
36
+ WMOff: {
37
+ command: 'Set',
38
+ dataKey: 'washerDryer.state',
39
+ dataValue: 'OFF',
40
+ },
41
+ },
42
+ };
43
+
44
+ // Act
45
+ const result = extractWasherStopCommand(deviceModel);
46
+
47
+ // Assert
48
+ expect(result).toBeDefined();
49
+ expect(result?.dataValue).toBe('OFF');
50
+ });
51
+
52
+ it('should prefer WMStop over WMOff when both present', () => {
53
+ // Arrange
54
+ const deviceModel = {
55
+ ControlWifi: {
56
+ WMStop: {
57
+ command: 'Set',
58
+ dataKey: 'washerDryer.state',
59
+ dataValue: 'STOP',
60
+ },
61
+ WMOff: {
62
+ command: 'Set',
63
+ dataKey: 'washerDryer.state',
64
+ dataValue: 'OFF',
65
+ },
66
+ },
67
+ };
68
+
69
+ // Act
70
+ const result = extractWasherStopCommand(deviceModel);
71
+
72
+ // Assert
73
+ expect(result?.dataValue).toBe('STOP');
74
+ });
75
+
76
+ it('should return undefined when both WMStop and WMOff are absent', () => {
77
+ // Arrange
78
+ const deviceModel = {
79
+ ControlWifi: {
80
+ OtherCommand: {
81
+ dataKey: 'some.state',
82
+ dataValue: 'VALUE',
83
+ },
84
+ },
85
+ };
86
+
87
+ // Act
88
+ const result = extractWasherStopCommand(deviceModel);
89
+
90
+ // Assert
91
+ expect(result).toBeUndefined();
92
+ });
93
+
94
+ it('should return undefined when ControlWifi is absent', () => {
95
+ // Arrange
96
+ const deviceModel = {
97
+ OtherProperty: {
98
+ WMStop: {
99
+ dataKey: 'washerDryer.state',
100
+ dataValue: 'STOP',
101
+ },
102
+ },
103
+ };
104
+
105
+ // Act
106
+ const result = extractWasherStopCommand(deviceModel);
107
+
108
+ // Assert
109
+ expect(result).toBeUndefined();
110
+ });
111
+
112
+ it('should return undefined for empty deviceModel', () => {
113
+ // Arrange
114
+ const deviceModel = {};
115
+
116
+ // Act
117
+ const result = extractWasherStopCommand(deviceModel);
118
+
119
+ // Assert
120
+ expect(result).toBeUndefined();
121
+ });
122
+
123
+ it('should return undefined when deviceModel is not an object', () => {
124
+ // Arrange
125
+ const deviceModel: any = null;
126
+
127
+ // Act
128
+ const result = extractWasherStopCommand(deviceModel);
129
+
130
+ // Assert
131
+ expect(result).toBeUndefined();
132
+ });
133
+
134
+ it('should handle WMStop as non-object gracefully', () => {
135
+ // Arrange
136
+ const deviceModel = {
137
+ ControlWifi: {
138
+ WMStop: 'string-value',
139
+ WMOff: {
140
+ dataKey: 'washerDryer.state',
141
+ dataValue: 'OFF',
142
+ },
143
+ },
144
+ };
145
+
146
+ // Act
147
+ const result = extractWasherStopCommand(deviceModel);
148
+
149
+ // Assert
150
+ expect(result).toBeDefined();
151
+ expect(result?.dataValue).toBe('OFF');
152
+ });
153
+
154
+ it('should include optional command field when present', () => {
155
+ // Arrange
156
+ const deviceModel = {
157
+ ControlWifi: {
158
+ WMStop: {
159
+ command: 'Operation',
160
+ dataKey: 'washerDryer.state',
161
+ dataValue: 'STOP',
162
+ },
163
+ },
164
+ };
165
+
166
+ // Act
167
+ const result = extractWasherStopCommand(deviceModel);
168
+
169
+ // Assert
170
+ expect(result?.command).toBe('Operation');
171
+ });
172
+
173
+ it('should omit command field when absent', () => {
174
+ // Arrange
175
+ const deviceModel = {
176
+ ControlWifi: {
177
+ WMStop: {
178
+ dataKey: 'washerDryer.state',
179
+ dataValue: 'STOP',
180
+ },
181
+ },
182
+ };
183
+
184
+ // Act
185
+ const result = extractWasherStopCommand(deviceModel);
186
+
187
+ // Assert
188
+ expect(result?.command).toBeUndefined();
189
+ });
190
+
191
+ it('should handle dataSetList when present', () => {
192
+ // Arrange
193
+ const deviceModel = {
194
+ ControlWifi: {
195
+ WMStop: {
196
+ dataKey: 'washerDryer.state',
197
+ dataValue: 'STOP',
198
+ dataSetList: {
199
+ course: 'value1',
200
+ spin: 'value2',
201
+ },
202
+ },
203
+ },
204
+ };
205
+
206
+ // Act
207
+ const result = extractWasherStopCommand(deviceModel);
208
+
209
+ // Assert
210
+ expect(result?.dataSetList).toBeDefined();
211
+ expect(result?.dataSetList?.course).toBe('value1');
212
+ expect(result?.dataSetList?.spin).toBe('value2');
213
+ });
214
+
215
+ it('should fallback to data field when dataSetList is absent', () => {
216
+ // Arrange
217
+ const deviceModel = {
218
+ ControlWifi: {
219
+ WMStop: {
220
+ dataKey: 'washerDryer.state',
221
+ dataValue: 'STOP',
222
+ data: {
223
+ course: 'value1',
224
+ },
225
+ },
226
+ },
227
+ };
228
+
229
+ // Act
230
+ const result = extractWasherStopCommand(deviceModel);
231
+
232
+ // Assert
233
+ expect(result?.dataSetList).toBeDefined();
234
+ expect(result?.dataSetList?.course).toBe('value1');
235
+ });
236
+
237
+ it('should prefer dataSetList over data field', () => {
238
+ // Arrange
239
+ const deviceModel = {
240
+ ControlWifi: {
241
+ WMStop: {
242
+ dataKey: 'washerDryer.state',
243
+ dataValue: 'STOP',
244
+ dataSetList: {
245
+ course: 'from-dataSetList',
246
+ },
247
+ data: {
248
+ course: 'from-data',
249
+ },
250
+ },
251
+ },
252
+ };
253
+
254
+ // Act
255
+ const result = extractWasherStopCommand(deviceModel);
256
+
257
+ // Assert
258
+ expect(result?.dataSetList?.course).toBe('from-dataSetList');
259
+ });
260
+
261
+ it('should handle dataValue as any type (not just string)', () => {
262
+ // Arrange
263
+ const deviceModel = {
264
+ ControlWifi: {
265
+ WMStop: {
266
+ dataKey: 'washerDryer.state',
267
+ dataValue: 123,
268
+ },
269
+ },
270
+ };
271
+
272
+ // Act
273
+ const result = extractWasherStopCommand(deviceModel);
274
+
275
+ // Assert
276
+ expect(result?.dataValue).toBe(123);
277
+ });
278
+
279
+ it('should handle boolean dataValue', () => {
280
+ // Arrange
281
+ const deviceModel = {
282
+ ControlWifi: {
283
+ WMStop: {
284
+ dataKey: 'washerDryer.state',
285
+ dataValue: true,
286
+ },
287
+ },
288
+ };
289
+
290
+ // Act
291
+ const result = extractWasherStopCommand(deviceModel);
292
+
293
+ // Assert
294
+ expect(result?.dataValue).toBe(true);
295
+ });
296
+
297
+ it('should handle null dataValue', () => {
298
+ // Arrange
299
+ const deviceModel = {
300
+ ControlWifi: {
301
+ WMStop: {
302
+ dataKey: 'washerDryer.state',
303
+ dataValue: null,
304
+ },
305
+ },
306
+ };
307
+
308
+ // Act
309
+ const result = extractWasherStopCommand(deviceModel);
310
+
311
+ // Assert
312
+ expect(result?.dataValue).toBeNull();
313
+ });
314
+
315
+ it('should omit dataSetList when neither dataSetList nor data are objects', () => {
316
+ // Arrange
317
+ const deviceModel = {
318
+ ControlWifi: {
319
+ WMStop: {
320
+ dataKey: 'washerDryer.state',
321
+ dataValue: 'STOP',
322
+ dataSetList: 'not-an-object',
323
+ data: 'also-not-an-object',
324
+ },
325
+ },
326
+ };
327
+
328
+ // Act
329
+ const result = extractWasherStopCommand(deviceModel);
330
+
331
+ // Assert
332
+ expect(result?.dataSetList).toBeUndefined();
333
+ });
334
+
335
+ it('should handle ControlWifi as non-object gracefully', () => {
336
+ // Arrange
337
+ const deviceModel = {
338
+ ControlWifi: 'not-an-object',
339
+ };
340
+
341
+ // Act
342
+ const result = extractWasherStopCommand(deviceModel);
343
+
344
+ // Assert
345
+ expect(result).toBeUndefined();
346
+ });
347
+
348
+ it('should handle deeply nested undefined properties gracefully', () => {
349
+ // Arrange
350
+ const deviceModel = {
351
+ ControlWifi: {
352
+ WMStop: {
353
+ dataKey: undefined,
354
+ dataValue: undefined,
355
+ },
356
+ },
357
+ };
358
+
359
+ // Act
360
+ const result = extractWasherStopCommand(deviceModel);
361
+
362
+ // Assert
363
+ expect(result).toBeDefined();
364
+ expect(result?.dataKey).toBeUndefined();
365
+ expect(result?.dataValue).toBeUndefined();
366
+ });
367
+
368
+ it('should include all fields from WMStop in payload', () => {
369
+ // Arrange
370
+ const deviceModel = {
371
+ ControlWifi: {
372
+ WMStop: {
373
+ command: 'Operation',
374
+ dataKey: 'washerDryer.state',
375
+ dataValue: 'STOP',
376
+ dataSetList: { course: 'express' },
377
+ },
378
+ },
379
+ };
380
+
381
+ // Act
382
+ const result = extractWasherStopCommand(deviceModel);
383
+
384
+ // Assert
385
+ expect(result).toBeDefined();
386
+ expect(result?.command).toBe('Operation');
387
+ expect(result?.dataKey).toBe('washerDryer.state');
388
+ expect(result?.dataValue).toBe('STOP');
389
+ expect(result?.dataSetList).toBeDefined();
390
+ });
391
+ });
392
+ });
@@ -0,0 +1,33 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { describe, expect, it } from 'vitest';
6
+
7
+ import { ENGINES, PLUGIN_NAME, PLUGIN_URL, PLUGIN_VERSION } from '../settings.js';
8
+
9
+ describe('settings', () => {
10
+ it('should export PLUGIN_NAME matching package.json name', () => {
11
+ const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
12
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as Record<string, unknown>;
13
+ expect(PLUGIN_NAME).toBe(pkg.name);
14
+ });
15
+
16
+ it('should export PLUGIN_VERSION matching package.json version', () => {
17
+ const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
18
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as Record<string, unknown>;
19
+ expect(PLUGIN_VERSION).toBe(pkg.version);
20
+ });
21
+
22
+ it('should export PLUGIN_URL matching package.json homepage', () => {
23
+ const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
24
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as Record<string, unknown>;
25
+ expect(PLUGIN_URL).toBe(pkg.homepage);
26
+ });
27
+
28
+ it('should export ENGINES matching package.json engines', () => {
29
+ const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
30
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as Record<string, unknown>;
31
+ expect(ENGINES).toEqual(pkg.engines);
32
+ });
33
+ });
@@ -0,0 +1,50 @@
1
+ /** Masks a secret string, leaving only the last `visibleChars` characters visible (e.g. `****abcd`). */
2
+ export function maskSecret(value: string | undefined, visibleChars = 4): string {
3
+ if (!value) {
4
+ return '';
5
+ }
6
+
7
+ if (value.length <= visibleChars) {
8
+ return '*'.repeat(value.length);
9
+ }
10
+
11
+ return '*'.repeat(value.length - visibleChars) + value.slice(-visibleChars);
12
+ }
13
+
14
+ /** Field names (case-insensitive) whose values are masked by `maskSensitiveFields` before being logged. */
15
+ const SENSITIVE_FIELD_NAMES = new Set([
16
+ 'refresh_token',
17
+ 'refreshtoken',
18
+ 'access_token',
19
+ 'accesstoken',
20
+ 'password',
21
+ 'client_secret',
22
+ 'clientsecret',
23
+ 'secret',
24
+ 'x-emp-token',
25
+ 'authorization',
26
+ ]);
27
+
28
+ /**
29
+ * Returns a shallow-safe, deep-masked copy of a plain object/record for debug logging: any key matching a known
30
+ * secret field name (refresh/access token, password, client secret, auth headers) is masked via `maskSecret`;
31
+ * everything else is copied as-is. Intended for logging outgoing request headers/bodies without leaking secrets.
32
+ */
33
+ export function maskSensitiveFields(source: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
34
+ if (!source) {
35
+ return source;
36
+ }
37
+
38
+ const result: Record<string, unknown> = {};
39
+ for (const [key, value] of Object.entries(source)) {
40
+ if (SENSITIVE_FIELD_NAMES.has(key.toLowerCase())) {
41
+ result[key] = typeof value === 'string' ? maskSecret(value) : value;
42
+ } else if (value && typeof value === 'object' && !Array.isArray(value)) {
43
+ result[key] = maskSensitiveFields(value as Record<string, unknown>);
44
+ } else {
45
+ result[key] = value;
46
+ }
47
+ }
48
+
49
+ return result;
50
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "incremental": true,
4
+ "composite": true,
5
+ "target": "esnext",
6
+ "lib": ["ESNext"],
7
+ "module": "nodenext",
8
+ "moduleResolution": "nodenext",
9
+ "resolveJsonModule": true,
10
+ "allowJs": true,
11
+ "checkJs": true,
12
+ "declaration": true,
13
+ "declarationMap": true,
14
+ "sourceMap": true,
15
+ "preserveConstEnums": true,
16
+ "allowSyntheticDefaultImports": true,
17
+ "esModuleInterop": true,
18
+ "forceConsistentCasingInFileNames": true,
19
+ "strict": true,
20
+ "noImplicitAny": true,
21
+ "alwaysStrict": true,
22
+ "noImplicitOverride": true,
23
+ "skipLibCheck": true
24
+ }
25
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src/**/*.ts"],
8
+ "exclude": []
9
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "incremental": false,
5
+ "composite": false,
6
+ "declaration": false,
7
+ "declarationMap": false,
8
+ "sourceMap": false,
9
+ "removeComments": true
10
+ },
11
+ "include": ["src/**/*.ts"],
12
+ "exclude": ["**/*.spec.ts", "**/*.test.ts", "**/__test__/*"]
13
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "./tsconfig.base.json",
3
+ "include": ["**/*.spec.ts", "**/*.test.ts", "**/tests/**/*.ts"],
4
+ "exclude": []
5
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,43 @@
1
+ // eslint-disable
2
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3
+ // @ts-nocheck
4
+ // vite.config.ts
5
+
6
+ // This Vitest configuration is designed for a TypeScript project.
7
+
8
+ import { defineConfig } from 'vitest/config';
9
+ import { fileURLToPath } from 'node:url';
10
+ import tsconfigPaths from 'vite-tsconfig-paths';
11
+
12
+ export default defineConfig({
13
+ plugins: [tsconfigPaths()],
14
+ test: {
15
+ include: ['src/**/*.test.ts', 'src/**/*.spec.ts'],
16
+ exclude: ['dist', 'node_modules', 'src/pluginTemplate/**', 'src/cli/**', 'src/cli.ts'],
17
+ globals: true,
18
+ clearMocks: true,
19
+ restoreMocks: true,
20
+ environment: 'node',
21
+ maxWorkers: 100,
22
+ coverage: {
23
+ provider: 'v8', // default, but explicit
24
+ reporter: ['text', 'lcov'],
25
+ include: ['src/**/*.{ts,tsx,js,jsx}'],
26
+ exclude: [
27
+ // Exclude test files that may live under src
28
+ 'src/**/*.test.{ts,tsx,js,jsx}',
29
+ 'src/**/*.spec.{ts,tsx,js,jsx}',
30
+ // Exclude helper file(s) not meant for coverage
31
+ 'src/tests/**/*.ts',
32
+ // Exclude type declaration files
33
+ 'src/**/*.d.ts',
34
+ ],
35
+ thresholds: {
36
+ statements: 75,
37
+ branches: 75,
38
+ functions: 75,
39
+ lines: 75,
40
+ },
41
+ },
42
+ },
43
+ });
@@ -0,0 +1,12 @@
1
+ # Memory Archive
2
+
3
+ Older entries pruned from `.claude/memory.md` to respect its per-section cap. Consult when working on the areas named below.
4
+
5
+ ## Common Pitfalls (archived)
6
+
7
+ - `classifyIslands` (`legacyIslandDetection.ts`) size-heuristic bug (Jul 27, fixed): was gating per `hasNoWallOrZoneData` map-wide; now evaluates per-island as last-resort fallback — deleted 4-line variable, removed gate from condition, updated doc comment.
8
+ - Implementer must NOT run full builds or the whole test suite — its gate is `format:ci` → `lint:fix:ci` → `type-check:ci` only; `build:local:ci`/`test:ci` belong to compiler/test-writer.
9
+ - `platformRunner.ts:120` writes `activeMapId` BEFORE `handleActiveMapChanged` — a guard inside the handler can't prevent `activeMapId` desync, and the same-map guard (`:119`) then swallows an identical-mapId retry.
10
+ - `SELECT_AREAS` empty-input path (`roborockVacuumCleaner.ts:164-176`) must NOT call `trySwitchMap` — keep empty vs explicit branches structurally separate with early `return`, else V10/V1 (`activeMapId=-1`) fires unguarded `switchMap` on every global-clean.
11
+ - ESLint `preserve-caught-error` requires re-thrown errors to carry `{ cause: err }` — omitting it fails `lint:fix:ci` even when the message embeds the original error text.
12
+ - ESLint `no-base-to-string` prohibits stringifying `err.cause` directly in templates (`String(err.cause)`) — check `if (cause instanceof Error)` first, then safely access `.message`; this prevents accidental `[object Object]` in logs.
@@ -0,0 +1,23 @@
1
+ # Reference Workspaces (allowlist)
2
+
3
+ External codebases agents may consult, **read-only**. Investigator and technical-architect may ONLY read paths listed here — anything not listed is out of bounds.
4
+
5
+ **Priority = list order.** Agents consult top-first and stop early once the question is answered.
6
+
7
+ Added for the `migrate-lg-thinq-to-matterbridge` task (Sep 8, 2026).
8
+
9
+ | Name | Path | Notes |
10
+ | ------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11
+ | matterbridge-example-dynamic-platform | `/Volumes/ExternalSSD/code/references/matterbridge-example-dynamic-platform` | Canonical Matterbridge dynamic-platform plugin shape: lifecycle hooks, device factories (`matterbridge/devices`), config schema, build tooling. |
12
+ | homebridge-lg-thinq | `/Volumes/ExternalSSD/code/references/homebridge-lg-thinq` | Source logic to port: LG ThinQ cloud OAuth/auth flow, API client, device discovery, device type classes, capability mapping, MQTT/polling event model. |
13
+ | ioBroker.lg-thinq | `/Volumes/ExternalSSD/code/references/ioBroker.lg-thinq` | Second, independent LG ThinQ cloud implementation (JS, by TA2k, v1.2.2): per-device-type modules in `lib/` (e.g. `air_conditioning.js`, `heat_pump.js`), model JSON capability tables in `lib/modelJsonUri`, `main.js` auth/poll/MQTT flow. Use to cross-check payloads/scales/field names against homebridge-lg-thinq. |
14
+ | rethink | `/Volumes/ExternalSSD/code/references/rethink` | anszom/rethink (GPL-2.0, TypeScript): a LOCAL replacement for the LG ThinQ cloud that talks to appliances over LG's local wire protocol (TLV over a local MQTT broker) and exposes them to Home Assistant. READ ONLY for ideas, never copy code (license). Best for how the AC itself behaves: `cloud/devices/RAC_056905_WW.ts` is the user's exact AC model (field-to-feature mapping, ranges, quirks such as the energy sensor bias). `bridge/thinqApi.ts`, `oauth2.ts`, `thinq2connection.ts`, `tools/lgcloud-monitor.ts` show how the real cloud is reached. It does NOT use the official control API, so it cannot explain cloud error codes or `airState.*` names; match fields by meaning. |
15
+ | homebridge-webos-tv | `/Volumes/ExternalSSD/code/references/homebridge-webos-tv` | SECOND source to port: LG webOS TV control over local network/websocket (SSAP protocol), NOT the ThinQ cloud API — distinct auth/connection model (local client-key pairing). |
16
+ | matterbridge-roborock-vacuum-plugin | `/Volumes/ExternalSSD/code/matterbridge-roborock-vacuum-plugin` | Sibling plugin, closest structural precedent for a prior Homebridge→Matterbridge migration in this family (folder layout, matterbridge API usage, config/build tooling, node-persist token storage). |
17
+
18
+ ## Rules
19
+
20
+ - Read-only — never modify, format, or stage files in these paths.
21
+ - CodeGraph does NOT work here (it indexes this repo only) — use Grep/Glob/Read.
22
+ - Cite findings as `<workspace-name>/path/to/file:line` in answers.
23
+ - To add a workspace: append a row here (with a one-line note on what it is good for) — no other change needed.