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,1035 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { ThinqSnapshot } from '../../../../core/domain/value-objects/ThinqSnapshot.js';
4
+
5
+ describe('ThinqSnapshot', () => {
6
+ describe('raw getter', () => {
7
+ it('should return the constructor input as-is', () => {
8
+ // Arrange
9
+ const data = {
10
+ 'airState.operation': 1,
11
+ 'airState.tempState.current': 22,
12
+ foo: 'bar',
13
+ };
14
+
15
+ // Act
16
+ const snapshot = new ThinqSnapshot(data);
17
+
18
+ // Assert
19
+ expect(snapshot.raw).toBe(data);
20
+ });
21
+ });
22
+
23
+ describe('isPowerOn', () => {
24
+ it('should return true when airState.operation is 1', () => {
25
+ // Arrange
26
+ const snapshot = new ThinqSnapshot({ 'airState.operation': 1 });
27
+
28
+ // Assert
29
+ expect(snapshot.isPowerOn).toBe(true);
30
+ });
31
+
32
+ it('should return false when airState.operation is 0', () => {
33
+ // Arrange
34
+ const snapshot = new ThinqSnapshot({ 'airState.operation': 0 });
35
+
36
+ // Assert
37
+ expect(snapshot.isPowerOn).toBe(false);
38
+ });
39
+
40
+ it('should return false when airState.operation is absent', () => {
41
+ // Arrange
42
+ const snapshot = new ThinqSnapshot({});
43
+
44
+ // Assert
45
+ expect(snapshot.isPowerOn).toBe(false);
46
+ });
47
+
48
+ it('should return false when airState.operation is a non-numeric type', () => {
49
+ // Arrange
50
+ const snapshot = new ThinqSnapshot({ 'airState.operation': 'on' });
51
+
52
+ // Assert
53
+ expect(snapshot.isPowerOn).toBe(false);
54
+ });
55
+ });
56
+
57
+ describe('currentTemperatureCelsius', () => {
58
+ it('should return the number value when present', () => {
59
+ // Arrange
60
+ const snapshot = new ThinqSnapshot({ 'airState.tempState.current': 22 });
61
+
62
+ // Assert
63
+ expect(snapshot.currentTemperatureCelsius).toBe(22);
64
+ });
65
+
66
+ it('should return undefined when key is absent', () => {
67
+ // Arrange
68
+ const snapshot = new ThinqSnapshot({});
69
+
70
+ // Assert
71
+ expect(snapshot.currentTemperatureCelsius).toBeUndefined();
72
+ });
73
+
74
+ it('should return undefined when value is not a number', () => {
75
+ // Arrange
76
+ const snapshot = new ThinqSnapshot({ 'airState.tempState.current': '22' });
77
+
78
+ // Assert
79
+ expect(snapshot.currentTemperatureCelsius).toBeUndefined();
80
+ });
81
+ });
82
+
83
+ describe('targetTemperatureCelsius', () => {
84
+ it('should return the number value when present', () => {
85
+ // Arrange
86
+ const snapshot = new ThinqSnapshot({ 'airState.tempState.target': 24 });
87
+
88
+ // Assert
89
+ expect(snapshot.targetTemperatureCelsius).toBe(24);
90
+ });
91
+
92
+ it('should return undefined when key is absent', () => {
93
+ // Arrange
94
+ const snapshot = new ThinqSnapshot({});
95
+
96
+ // Assert
97
+ expect(snapshot.targetTemperatureCelsius).toBeUndefined();
98
+ });
99
+
100
+ it('should return undefined when value is not a number', () => {
101
+ // Arrange
102
+ const snapshot = new ThinqSnapshot({ 'airState.tempState.target': '24' });
103
+
104
+ // Assert
105
+ expect(snapshot.targetTemperatureCelsius).toBeUndefined();
106
+ });
107
+ });
108
+
109
+ describe('operationMode', () => {
110
+ it('should return the number value when present', () => {
111
+ // Arrange
112
+ const snapshot = new ThinqSnapshot({ 'airState.opMode': 0 });
113
+
114
+ // Assert
115
+ expect(snapshot.operationMode).toBe(0);
116
+ });
117
+
118
+ it('should return undefined when key is absent', () => {
119
+ // Arrange
120
+ const snapshot = new ThinqSnapshot({});
121
+
122
+ // Assert
123
+ expect(snapshot.operationMode).toBeUndefined();
124
+ });
125
+
126
+ it('should return undefined when value is not a number', () => {
127
+ // Arrange
128
+ const snapshot = new ThinqSnapshot({ 'airState.opMode': 'cool' });
129
+
130
+ // Assert
131
+ expect(snapshot.operationMode).toBeUndefined();
132
+ });
133
+ });
134
+
135
+ describe('windStrength', () => {
136
+ it('should return the number value when present', () => {
137
+ // Arrange
138
+ const snapshot = new ThinqSnapshot({ 'airState.windStrength': 2 });
139
+
140
+ // Assert
141
+ expect(snapshot.windStrength).toBe(2);
142
+ });
143
+
144
+ it('should return undefined when key is absent', () => {
145
+ // Arrange
146
+ const snapshot = new ThinqSnapshot({});
147
+
148
+ // Assert
149
+ expect(snapshot.windStrength).toBeUndefined();
150
+ });
151
+
152
+ it('should return undefined when value is not a number', () => {
153
+ // Arrange
154
+ const snapshot = new ThinqSnapshot({ 'airState.windStrength': 'low' });
155
+
156
+ // Assert
157
+ expect(snapshot.windStrength).toBeUndefined();
158
+ });
159
+ });
160
+
161
+ describe('online', () => {
162
+ it('should return true when online is boolean true', () => {
163
+ // Arrange
164
+ const snapshot = new ThinqSnapshot({ online: true });
165
+
166
+ // Assert
167
+ expect(snapshot.online).toBe(true);
168
+ });
169
+
170
+ it('should return false when online is boolean false', () => {
171
+ // Arrange
172
+ const snapshot = new ThinqSnapshot({ online: false });
173
+
174
+ // Assert
175
+ expect(snapshot.online).toBe(false);
176
+ });
177
+
178
+ it('should return undefined when online is absent', () => {
179
+ // Arrange
180
+ const snapshot = new ThinqSnapshot({});
181
+
182
+ // Assert
183
+ expect(snapshot.online).toBeUndefined();
184
+ });
185
+
186
+ it('should return undefined when online is not a boolean', () => {
187
+ // Arrange
188
+ const snapshot = new ThinqSnapshot({ online: 'yes' });
189
+
190
+ // Assert
191
+ expect(snapshot.online).toBeUndefined();
192
+ });
193
+ });
194
+
195
+ describe('isJetModeOn', () => {
196
+ it('should return true when airState.wMode.jet is 1', () => {
197
+ // Arrange
198
+ const snapshot = new ThinqSnapshot({ 'airState.wMode.jet': 1 });
199
+
200
+ // Assert
201
+ expect(snapshot.isJetModeOn).toBe(true);
202
+ });
203
+
204
+ it('should return false when airState.wMode.jet is 0', () => {
205
+ // Arrange
206
+ const snapshot = new ThinqSnapshot({ 'airState.wMode.jet': 0 });
207
+
208
+ // Assert
209
+ expect(snapshot.isJetModeOn).toBe(false);
210
+ });
211
+
212
+ it('should return false when airState.wMode.jet is absent', () => {
213
+ // Arrange
214
+ const snapshot = new ThinqSnapshot({});
215
+
216
+ // Assert
217
+ expect(snapshot.isJetModeOn).toBe(false);
218
+ });
219
+ });
220
+
221
+ describe('isQuietModeOn', () => {
222
+ it('should return true when airState.miscFuncState.silentAWHP is 1', () => {
223
+ // Arrange
224
+ const snapshot = new ThinqSnapshot({ 'airState.miscFuncState.silentAWHP': 1 });
225
+
226
+ // Assert
227
+ expect(snapshot.isQuietModeOn).toBe(true);
228
+ });
229
+
230
+ it('should return false when airState.miscFuncState.silentAWHP is 0', () => {
231
+ // Arrange
232
+ const snapshot = new ThinqSnapshot({ 'airState.miscFuncState.silentAWHP': 0 });
233
+
234
+ // Assert
235
+ expect(snapshot.isQuietModeOn).toBe(false);
236
+ });
237
+
238
+ it('should return false when airState.miscFuncState.silentAWHP is absent', () => {
239
+ // Arrange
240
+ const snapshot = new ThinqSnapshot({});
241
+
242
+ // Assert
243
+ expect(snapshot.isQuietModeOn).toBe(false);
244
+ });
245
+ });
246
+
247
+ describe('isEnergySaveModeOn', () => {
248
+ it('should return true when airState.powerSave.basic is 1', () => {
249
+ // Arrange
250
+ const snapshot = new ThinqSnapshot({ 'airState.powerSave.basic': 1 });
251
+
252
+ // Assert
253
+ expect(snapshot.isEnergySaveModeOn).toBe(true);
254
+ });
255
+
256
+ it('should return false when airState.powerSave.basic is 0', () => {
257
+ // Arrange
258
+ const snapshot = new ThinqSnapshot({ 'airState.powerSave.basic': 0 });
259
+
260
+ // Assert
261
+ expect(snapshot.isEnergySaveModeOn).toBe(false);
262
+ });
263
+
264
+ it('should return false when airState.powerSave.basic is absent', () => {
265
+ // Arrange
266
+ const snapshot = new ThinqSnapshot({});
267
+
268
+ // Assert
269
+ expect(snapshot.isEnergySaveModeOn).toBe(false);
270
+ });
271
+ });
272
+
273
+ describe('isAirCleanModeOn', () => {
274
+ it('should return true when airState.wMode.airClean is 1', () => {
275
+ // Arrange
276
+ const snapshot = new ThinqSnapshot({ 'airState.wMode.airClean': 1 });
277
+
278
+ // Assert
279
+ expect(snapshot.isAirCleanModeOn).toBe(true);
280
+ });
281
+
282
+ it('should return false when airState.wMode.airClean is 0', () => {
283
+ // Arrange
284
+ const snapshot = new ThinqSnapshot({ 'airState.wMode.airClean': 0 });
285
+
286
+ // Assert
287
+ expect(snapshot.isAirCleanModeOn).toBe(false);
288
+ });
289
+
290
+ it('should return false when airState.wMode.airClean is absent', () => {
291
+ // Arrange
292
+ const snapshot = new ThinqSnapshot({});
293
+
294
+ // Assert
295
+ expect(snapshot.isAirCleanModeOn).toBe(false);
296
+ });
297
+ });
298
+
299
+ describe('isLedOn', () => {
300
+ it('should return true when airState.lightingState.displayControl is 1', () => {
301
+ // Arrange
302
+ const snapshot = new ThinqSnapshot({ 'airState.lightingState.displayControl': 1 });
303
+
304
+ // Assert
305
+ expect(snapshot.isLedOn).toBe(true);
306
+ });
307
+
308
+ it('should return false when airState.lightingState.displayControl is 0', () => {
309
+ // Arrange
310
+ const snapshot = new ThinqSnapshot({ 'airState.lightingState.displayControl': 0 });
311
+
312
+ // Assert
313
+ expect(snapshot.isLedOn).toBe(false);
314
+ });
315
+
316
+ it('should return false when airState.lightingState.displayControl is absent', () => {
317
+ // Arrange
318
+ const snapshot = new ThinqSnapshot({});
319
+
320
+ // Assert
321
+ expect(snapshot.isLedOn).toBe(false);
322
+ });
323
+ });
324
+
325
+ describe('isVerticalSwingOn (Phase B)', () => {
326
+ it('should return true when airState.wDir.vStep is 100 (numeric)', () => {
327
+ // Arrange
328
+ const snapshot = new ThinqSnapshot({ 'airState.wDir.vStep': 100 });
329
+
330
+ // Assert
331
+ expect(snapshot.isVerticalSwingOn).toBe(true);
332
+ });
333
+
334
+ it('should return true when airState.wDir.vStep is "100" (string)', () => {
335
+ // Arrange
336
+ const snapshot = new ThinqSnapshot({ 'airState.wDir.vStep': '100' });
337
+
338
+ // Assert
339
+ expect(snapshot.isVerticalSwingOn).toBe(true);
340
+ });
341
+
342
+ it('should return false when airState.wDir.vStep is 0', () => {
343
+ // Arrange
344
+ const snapshot = new ThinqSnapshot({ 'airState.wDir.vStep': 0 });
345
+
346
+ // Assert
347
+ expect(snapshot.isVerticalSwingOn).toBe(false);
348
+ });
349
+
350
+ it('should return false when airState.wDir.vStep is absent', () => {
351
+ // Arrange
352
+ const snapshot = new ThinqSnapshot({});
353
+
354
+ // Assert
355
+ expect(snapshot.isVerticalSwingOn).toBe(false);
356
+ });
357
+
358
+ it('should return false when airState.wDir.vStep is any other number', () => {
359
+ // Arrange
360
+ const snapshot = new ThinqSnapshot({ 'airState.wDir.vStep': 50 });
361
+
362
+ // Assert
363
+ expect(snapshot.isVerticalSwingOn).toBe(false);
364
+ });
365
+ });
366
+
367
+ describe('isHorizontalSwingOn (Phase B)', () => {
368
+ it('should return true when airState.wDir.hStep is 100 (numeric)', () => {
369
+ // Arrange
370
+ const snapshot = new ThinqSnapshot({ 'airState.wDir.hStep': 100 });
371
+
372
+ // Assert
373
+ expect(snapshot.isHorizontalSwingOn).toBe(true);
374
+ });
375
+
376
+ it('should return true when airState.wDir.hStep is "100" (string)', () => {
377
+ // Arrange
378
+ const snapshot = new ThinqSnapshot({ 'airState.wDir.hStep': '100' });
379
+
380
+ // Assert
381
+ expect(snapshot.isHorizontalSwingOn).toBe(true);
382
+ });
383
+
384
+ it('should return false when airState.wDir.hStep is 0', () => {
385
+ // Arrange
386
+ const snapshot = new ThinqSnapshot({ 'airState.wDir.hStep': 0 });
387
+
388
+ // Assert
389
+ expect(snapshot.isHorizontalSwingOn).toBe(false);
390
+ });
391
+
392
+ it('should return false when airState.wDir.hStep is absent', () => {
393
+ // Arrange
394
+ const snapshot = new ThinqSnapshot({});
395
+
396
+ // Assert
397
+ expect(snapshot.isHorizontalSwingOn).toBe(false);
398
+ });
399
+
400
+ it('should return false when airState.wDir.hStep is any other number', () => {
401
+ // Arrange
402
+ const snapshot = new ThinqSnapshot({ 'airState.wDir.hStep': 50 });
403
+
404
+ // Assert
405
+ expect(snapshot.isHorizontalSwingOn).toBe(false);
406
+ });
407
+ });
408
+
409
+ describe('humidityPercent (Phase C)', () => {
410
+ it('should return the value directly when <= 100', () => {
411
+ // Arrange
412
+ const snapshot = new ThinqSnapshot({ 'airState.humidity.current': 55 });
413
+
414
+ // Assert
415
+ expect(snapshot.humidityPercent).toBe(55);
416
+ });
417
+
418
+ it('should return the value divided by 10 when > 100', () => {
419
+ // Arrange
420
+ const snapshot = new ThinqSnapshot({ 'airState.humidity.current': 550 });
421
+
422
+ // Assert
423
+ expect(snapshot.humidityPercent).toBe(55);
424
+ });
425
+
426
+ it('should return exactly 100 when value is 100', () => {
427
+ // Arrange
428
+ const snapshot = new ThinqSnapshot({ 'airState.humidity.current': 100 });
429
+
430
+ // Assert
431
+ expect(snapshot.humidityPercent).toBe(100);
432
+ });
433
+
434
+ it('should return undefined when key is absent', () => {
435
+ // Arrange
436
+ const snapshot = new ThinqSnapshot({});
437
+
438
+ // Assert
439
+ expect(snapshot.humidityPercent).toBeUndefined();
440
+ });
441
+
442
+ it('should return undefined when value is not a number', () => {
443
+ // Arrange
444
+ const snapshot = new ThinqSnapshot({ 'airState.humidity.current': '55' });
445
+
446
+ // Assert
447
+ expect(snapshot.humidityPercent).toBeUndefined();
448
+ });
449
+
450
+ it('should apply heuristic to large values (1000 → 100)', () => {
451
+ // Arrange
452
+ const snapshot = new ThinqSnapshot({ 'airState.humidity.current': 1000 });
453
+
454
+ // Assert
455
+ expect(snapshot.humidityPercent).toBe(100);
456
+ });
457
+
458
+ it('should apply heuristic to small > 100 values (101 → 10.1)', () => {
459
+ // Arrange
460
+ const snapshot = new ThinqSnapshot({ 'airState.humidity.current': 101 });
461
+
462
+ // Assert
463
+ expect(snapshot.humidityPercent).toBe(10.1);
464
+ });
465
+
466
+ it('should apply heuristic to common > 100 value (650 → 65)', () => {
467
+ // Arrange
468
+ const snapshot = new ThinqSnapshot({ 'airState.humidity.current': 650 });
469
+
470
+ // Assert
471
+ expect(snapshot.humidityPercent).toBe(65);
472
+ });
473
+ });
474
+
475
+ describe('airQualityOverall (Phase C)', () => {
476
+ it('should return the number value when present', () => {
477
+ // Arrange
478
+ const snapshot = new ThinqSnapshot({ 'airState.quality.overall': 3 });
479
+
480
+ // Assert
481
+ expect(snapshot.airQualityOverall).toBe(3);
482
+ });
483
+
484
+ it('should return undefined when key is absent', () => {
485
+ // Arrange
486
+ const snapshot = new ThinqSnapshot({});
487
+
488
+ // Assert
489
+ expect(snapshot.airQualityOverall).toBeUndefined();
490
+ });
491
+
492
+ it('should return undefined when value is not a number', () => {
493
+ // Arrange
494
+ const snapshot = new ThinqSnapshot({ 'airState.quality.overall': 'Good' });
495
+
496
+ // Assert
497
+ expect(snapshot.airQualityOverall).toBeUndefined();
498
+ });
499
+
500
+ it('should return 0 for poor air quality', () => {
501
+ // Arrange
502
+ const snapshot = new ThinqSnapshot({ 'airState.quality.overall': 0 });
503
+
504
+ // Assert
505
+ expect(snapshot.airQualityOverall).toBe(0);
506
+ });
507
+ });
508
+
509
+ describe('pm25 (Phase C)', () => {
510
+ it('should return the number value when present', () => {
511
+ // Arrange
512
+ const snapshot = new ThinqSnapshot({ 'airState.quality.PM2': 25 });
513
+
514
+ // Assert
515
+ expect(snapshot.pm25).toBe(25);
516
+ });
517
+
518
+ it('should return undefined when key is absent', () => {
519
+ // Arrange
520
+ const snapshot = new ThinqSnapshot({});
521
+
522
+ // Assert
523
+ expect(snapshot.pm25).toBeUndefined();
524
+ });
525
+
526
+ it('should return undefined when value is not a number', () => {
527
+ // Arrange
528
+ const snapshot = new ThinqSnapshot({ 'airState.quality.PM2': '25' });
529
+
530
+ // Assert
531
+ expect(snapshot.pm25).toBeUndefined();
532
+ });
533
+
534
+ it('should return 0 for zero PM2.5 reading', () => {
535
+ // Arrange
536
+ const snapshot = new ThinqSnapshot({ 'airState.quality.PM2': 0 });
537
+
538
+ // Assert
539
+ expect(snapshot.pm25).toBe(0);
540
+ });
541
+ });
542
+
543
+ describe('pm10 (Phase C)', () => {
544
+ it('should return the number value when present', () => {
545
+ // Arrange
546
+ const snapshot = new ThinqSnapshot({ 'airState.quality.PM10': 50 });
547
+
548
+ // Assert
549
+ expect(snapshot.pm10).toBe(50);
550
+ });
551
+
552
+ it('should return undefined when key is absent', () => {
553
+ // Arrange
554
+ const snapshot = new ThinqSnapshot({});
555
+
556
+ // Assert
557
+ expect(snapshot.pm10).toBeUndefined();
558
+ });
559
+
560
+ it('should return undefined when value is not a number', () => {
561
+ // Arrange
562
+ const snapshot = new ThinqSnapshot({ 'airState.quality.PM10': '50' });
563
+
564
+ // Assert
565
+ expect(snapshot.pm10).toBeUndefined();
566
+ });
567
+
568
+ it('should return 0 for zero PM10 reading', () => {
569
+ // Arrange
570
+ const snapshot = new ThinqSnapshot({ 'airState.quality.PM10': 0 });
571
+
572
+ // Assert
573
+ expect(snapshot.pm10).toBe(0);
574
+ });
575
+ });
576
+
577
+ describe('powerConsumptionWatts (Phase D)', () => {
578
+ it('should return the raw value when present (no division)', () => {
579
+ // Arrange
580
+ const snapshot = new ThinqSnapshot({ 'airState.energy.onCurrent': 500 });
581
+
582
+ // Assert
583
+ expect(snapshot.powerConsumptionWatts).toBe(500);
584
+ });
585
+
586
+ it('should return 0 when airState.energy.onCurrent is 0', () => {
587
+ // Arrange
588
+ const snapshot = new ThinqSnapshot({ 'airState.energy.onCurrent': 0 });
589
+
590
+ // Assert
591
+ expect(snapshot.powerConsumptionWatts).toBe(0);
592
+ });
593
+
594
+ it('should return undefined when key is absent', () => {
595
+ // Arrange
596
+ const snapshot = new ThinqSnapshot({});
597
+
598
+ // Assert
599
+ expect(snapshot.powerConsumptionWatts).toBeUndefined();
600
+ });
601
+
602
+ it('should return undefined when value is not a number', () => {
603
+ // Arrange
604
+ const snapshot = new ThinqSnapshot({ 'airState.energy.onCurrent': '500' });
605
+
606
+ // Assert
607
+ expect(snapshot.powerConsumptionWatts).toBeUndefined();
608
+ });
609
+
610
+ it('should return undefined when the result is NaN', () => {
611
+ // Arrange
612
+ const snapshot = new ThinqSnapshot({ 'airState.energy.onCurrent': NaN });
613
+
614
+ // Assert
615
+ expect(snapshot.powerConsumptionWatts).toBeUndefined();
616
+ });
617
+
618
+ it('should return raw value for intermediate powers (999 → 999)', () => {
619
+ // Arrange
620
+ const snapshot = new ThinqSnapshot({ 'airState.energy.onCurrent': 999 });
621
+
622
+ // Assert
623
+ expect(snapshot.powerConsumptionWatts).toBe(999);
624
+ });
625
+
626
+ it('should handle large power values (5000 → 5000)', () => {
627
+ // Arrange
628
+ const snapshot = new ThinqSnapshot({ 'airState.energy.onCurrent': 5000 });
629
+
630
+ // Assert
631
+ expect(snapshot.powerConsumptionWatts).toBe(5000);
632
+ });
633
+
634
+ it('should handle fractional onCurrent values (1.5 → 1.5)', () => {
635
+ // Arrange
636
+ const snapshot = new ThinqSnapshot({ 'airState.energy.onCurrent': 1.5 });
637
+
638
+ // Assert
639
+ expect(snapshot.powerConsumptionWatts).toBe(1.5);
640
+ });
641
+ });
642
+
643
+ describe('has', () => {
644
+ it('should return true when key has a number value', () => {
645
+ // Arrange
646
+ const snapshot = new ThinqSnapshot({ 'airState.operation': 1 });
647
+
648
+ // Assert
649
+ expect(snapshot.has('airState.operation')).toBe(true);
650
+ });
651
+
652
+ it('should return true when key has value 0 (falsy number)', () => {
653
+ // Arrange
654
+ const snapshot = new ThinqSnapshot({ 'airState.operation': 0 });
655
+
656
+ // Assert
657
+ expect(snapshot.has('airState.operation')).toBe(true);
658
+ });
659
+
660
+ it('should return true when key has a false boolean value', () => {
661
+ // Arrange
662
+ const snapshot = new ThinqSnapshot({ online: false });
663
+
664
+ // Assert
665
+ expect(snapshot.has('online')).toBe(true);
666
+ });
667
+
668
+ it('should return true when key has an empty string value', () => {
669
+ // Arrange
670
+ const snapshot = new ThinqSnapshot({ foo: '' });
671
+
672
+ // Assert
673
+ expect(snapshot.has('foo')).toBe(true);
674
+ });
675
+
676
+ it('should return false when key is absent', () => {
677
+ // Arrange
678
+ const snapshot = new ThinqSnapshot({});
679
+
680
+ // Assert
681
+ expect(snapshot.has('airState.operation')).toBe(false);
682
+ });
683
+
684
+ it('should return false when key value is undefined', () => {
685
+ // Arrange
686
+ const snapshot = new ThinqSnapshot({ 'airState.operation': undefined });
687
+
688
+ // Assert
689
+ expect(snapshot.has('airState.operation')).toBe(false);
690
+ });
691
+
692
+ it('should return false when key value is null', () => {
693
+ // Arrange
694
+ const snapshot = new ThinqSnapshot({ 'airState.operation': null });
695
+
696
+ // Assert
697
+ expect(snapshot.has('airState.operation')).toBe(false);
698
+ });
699
+
700
+ it('regression: isPowerOn should remain false when key is absent (existing behavior)', () => {
701
+ // Arrange
702
+ const snapshot = new ThinqSnapshot({});
703
+
704
+ // Assert
705
+ expect(snapshot.isPowerOn).toBe(false);
706
+ });
707
+ });
708
+
709
+ describe('washerRawState', () => {
710
+ it('should return the washerDryer.state string when present', () => {
711
+ // Arrange
712
+ const snapshot = new ThinqSnapshot({
713
+ washerDryer: { state: 'RUNNING', remainTimeHour: 1 },
714
+ });
715
+
716
+ // Act & Assert
717
+ expect(snapshot.washerRawState).toBe('RUNNING');
718
+ });
719
+
720
+ it('should return undefined when washerDryer is absent', () => {
721
+ // Arrange
722
+ const snapshot = new ThinqSnapshot({});
723
+
724
+ // Act & Assert
725
+ expect(snapshot.washerRawState).toBeUndefined();
726
+ });
727
+
728
+ it('should return undefined when washerDryer.state is absent', () => {
729
+ // Arrange
730
+ const snapshot = new ThinqSnapshot({
731
+ washerDryer: { remainTimeHour: 1 },
732
+ });
733
+
734
+ // Act & Assert
735
+ expect(snapshot.washerRawState).toBeUndefined();
736
+ });
737
+
738
+ it('should return undefined when washerDryer.state is not a string', () => {
739
+ // Arrange
740
+ const snapshot = new ThinqSnapshot({
741
+ washerDryer: { state: 123 },
742
+ });
743
+
744
+ // Act & Assert
745
+ expect(snapshot.washerRawState).toBeUndefined();
746
+ });
747
+ });
748
+
749
+ describe('isWasherPowerOn', () => {
750
+ it('should return true when washerRawState is RUNNING', () => {
751
+ // Arrange
752
+ const snapshot = new ThinqSnapshot({
753
+ washerDryer: { state: 'RUNNING' },
754
+ });
755
+
756
+ // Act & Assert
757
+ expect(snapshot.isWasherPowerOn).toBe(true);
758
+ });
759
+
760
+ it('should return true when washerRawState is PAUSE', () => {
761
+ // Arrange
762
+ const snapshot = new ThinqSnapshot({
763
+ washerDryer: { state: 'PAUSE' },
764
+ });
765
+
766
+ // Act & Assert
767
+ expect(snapshot.isWasherPowerOn).toBe(true);
768
+ });
769
+
770
+ it('should return true when washerRawState is END', () => {
771
+ // Arrange
772
+ const snapshot = new ThinqSnapshot({
773
+ washerDryer: { state: 'END' },
774
+ });
775
+
776
+ // Act & Assert
777
+ expect(snapshot.isWasherPowerOn).toBe(true);
778
+ });
779
+
780
+ it('should return true when washerRawState is ERROR', () => {
781
+ // Arrange
782
+ const snapshot = new ThinqSnapshot({
783
+ washerDryer: { state: 'ERROR' },
784
+ });
785
+
786
+ // Act & Assert
787
+ expect(snapshot.isWasherPowerOn).toBe(true);
788
+ });
789
+
790
+ it('should return false when washerRawState is POWEROFF', () => {
791
+ // Arrange
792
+ const snapshot = new ThinqSnapshot({
793
+ washerDryer: { state: 'POWEROFF' },
794
+ });
795
+
796
+ // Act & Assert
797
+ expect(snapshot.isWasherPowerOn).toBe(false);
798
+ });
799
+
800
+ it('should return false when washerRawState is POWERFAIL', () => {
801
+ // Arrange
802
+ const snapshot = new ThinqSnapshot({
803
+ washerDryer: { state: 'POWERFAIL' },
804
+ });
805
+
806
+ // Act & Assert
807
+ expect(snapshot.isWasherPowerOn).toBe(false);
808
+ });
809
+
810
+ it('should return false when washerDryer is absent', () => {
811
+ // Arrange
812
+ const snapshot = new ThinqSnapshot({});
813
+
814
+ // Act & Assert
815
+ expect(snapshot.isWasherPowerOn).toBe(false);
816
+ });
817
+ });
818
+
819
+ describe('isWasherRunning', () => {
820
+ it('should return true when washerRawState is RUNNING', () => {
821
+ // Arrange
822
+ const snapshot = new ThinqSnapshot({
823
+ washerDryer: { state: 'RUNNING' },
824
+ });
825
+
826
+ // Act & Assert
827
+ expect(snapshot.isWasherRunning).toBe(true);
828
+ });
829
+
830
+ it('should return false when washerRawState is PAUSE (not running, even if powered on)', () => {
831
+ // Arrange
832
+ const snapshot = new ThinqSnapshot({
833
+ washerDryer: { state: 'PAUSE' },
834
+ });
835
+
836
+ // Act & Assert
837
+ expect(snapshot.isWasherRunning).toBe(false);
838
+ });
839
+
840
+ it('should return false when washerRawState is END (finished, not running)', () => {
841
+ // Arrange
842
+ const snapshot = new ThinqSnapshot({
843
+ washerDryer: { state: 'END' },
844
+ });
845
+
846
+ // Act & Assert
847
+ expect(snapshot.isWasherRunning).toBe(false);
848
+ });
849
+
850
+ it('should return false when washerRawState is POWEROFF', () => {
851
+ // Arrange
852
+ const snapshot = new ThinqSnapshot({
853
+ washerDryer: { state: 'POWEROFF' },
854
+ });
855
+
856
+ // Act & Assert
857
+ expect(snapshot.isWasherRunning).toBe(false);
858
+ });
859
+
860
+ it('should return false when washerRawState is ERROR', () => {
861
+ // Arrange
862
+ const snapshot = new ThinqSnapshot({
863
+ washerDryer: { state: 'ERROR' },
864
+ });
865
+
866
+ // Act & Assert
867
+ expect(snapshot.isWasherRunning).toBe(false);
868
+ });
869
+
870
+ it('should return false when washerDryer is absent', () => {
871
+ // Arrange
872
+ const snapshot = new ThinqSnapshot({});
873
+
874
+ // Act & Assert
875
+ expect(snapshot.isWasherRunning).toBe(false);
876
+ });
877
+ });
878
+
879
+ describe('isWasherError', () => {
880
+ it('should return true when washerRawState is ERROR', () => {
881
+ // Arrange
882
+ const snapshot = new ThinqSnapshot({
883
+ washerDryer: { state: 'ERROR' },
884
+ });
885
+
886
+ // Act & Assert
887
+ expect(snapshot.isWasherError).toBe(true);
888
+ });
889
+
890
+ it('should return false when washerRawState is RUNNING', () => {
891
+ // Arrange
892
+ const snapshot = new ThinqSnapshot({
893
+ washerDryer: { state: 'RUNNING' },
894
+ });
895
+
896
+ // Act & Assert
897
+ expect(snapshot.isWasherError).toBe(false);
898
+ });
899
+
900
+ it('should return false when washerRawState is POWEROFF', () => {
901
+ // Arrange
902
+ const snapshot = new ThinqSnapshot({
903
+ washerDryer: { state: 'POWEROFF' },
904
+ });
905
+
906
+ // Act & Assert
907
+ expect(snapshot.isWasherError).toBe(false);
908
+ });
909
+
910
+ it('should return false when washerDryer is absent', () => {
911
+ // Arrange
912
+ const snapshot = new ThinqSnapshot({});
913
+
914
+ // Act & Assert
915
+ expect(snapshot.isWasherError).toBe(false);
916
+ });
917
+ });
918
+
919
+ describe('washerRemainingDurationSeconds', () => {
920
+ it('should return combined seconds when both hours and minutes present', () => {
921
+ // Arrange
922
+ const snapshot = new ThinqSnapshot({
923
+ washerDryer: { state: 'RUNNING', remainTimeHour: 1, remainTimeMinute: 30 },
924
+ });
925
+
926
+ // Act
927
+ const result = snapshot.washerRemainingDurationSeconds;
928
+
929
+ // Assert
930
+ expect(result).toBe(5400); // 1*3600 + 30*60
931
+ });
932
+
933
+ it('should return seconds from hours only', () => {
934
+ // Arrange
935
+ const snapshot = new ThinqSnapshot({
936
+ washerDryer: { state: 'RUNNING', remainTimeHour: 2 },
937
+ });
938
+
939
+ // Act
940
+ const result = snapshot.washerRemainingDurationSeconds;
941
+
942
+ // Assert
943
+ expect(result).toBe(7200); // 2*3600
944
+ });
945
+
946
+ it('should return seconds from minutes only', () => {
947
+ // Arrange
948
+ const snapshot = new ThinqSnapshot({
949
+ washerDryer: { state: 'RUNNING', remainTimeMinute: 45 },
950
+ });
951
+
952
+ // Act
953
+ const result = snapshot.washerRemainingDurationSeconds;
954
+
955
+ // Assert
956
+ expect(result).toBe(2700); // 45*60
957
+ });
958
+
959
+ it('should return 0 when present but not running (PAUSE)', () => {
960
+ // Arrange
961
+ const snapshot = new ThinqSnapshot({
962
+ washerDryer: { state: 'PAUSE', remainTimeHour: 1, remainTimeMinute: 30 },
963
+ });
964
+
965
+ // Act
966
+ const result = snapshot.washerRemainingDurationSeconds;
967
+
968
+ // Assert
969
+ expect(result).toBe(0); // Zeroed when not running
970
+ });
971
+
972
+ it('should return 0 when present but not running (END)', () => {
973
+ // Arrange
974
+ const snapshot = new ThinqSnapshot({
975
+ washerDryer: { state: 'END', remainTimeHour: 1 },
976
+ });
977
+
978
+ // Act
979
+ const result = snapshot.washerRemainingDurationSeconds;
980
+
981
+ // Assert
982
+ expect(result).toBe(0); // Zeroed when not running
983
+ });
984
+
985
+ it('should return undefined when both remain times absent', () => {
986
+ // Arrange
987
+ const snapshot = new ThinqSnapshot({
988
+ washerDryer: { state: 'RUNNING' },
989
+ });
990
+
991
+ // Act
992
+ const result = snapshot.washerRemainingDurationSeconds;
993
+
994
+ // Assert
995
+ expect(result).toBeUndefined();
996
+ });
997
+
998
+ it('should return undefined when washerDryer absent', () => {
999
+ // Arrange
1000
+ const snapshot = new ThinqSnapshot({});
1001
+
1002
+ // Act
1003
+ const result = snapshot.washerRemainingDurationSeconds;
1004
+
1005
+ // Assert
1006
+ expect(result).toBeUndefined();
1007
+ });
1008
+
1009
+ it('should handle zero hour/minute values', () => {
1010
+ // Arrange
1011
+ const snapshot = new ThinqSnapshot({
1012
+ washerDryer: { state: 'RUNNING', remainTimeHour: 0, remainTimeMinute: 0 },
1013
+ });
1014
+
1015
+ // Act
1016
+ const result = snapshot.washerRemainingDurationSeconds;
1017
+
1018
+ // Assert
1019
+ expect(result).toBe(0);
1020
+ });
1021
+
1022
+ it('should add hours and minutes correctly (large values)', () => {
1023
+ // Arrange
1024
+ const snapshot = new ThinqSnapshot({
1025
+ washerDryer: { state: 'RUNNING', remainTimeHour: 10, remainTimeMinute: 59 },
1026
+ });
1027
+
1028
+ // Act
1029
+ const result = snapshot.washerRemainingDurationSeconds;
1030
+
1031
+ // Assert
1032
+ expect(result).toBe(39540); // 10*3600 + 59*60
1033
+ });
1034
+ });
1035
+ });