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
package/CHANGELOG.md ADDED
@@ -0,0 +1,195 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0-rc13] - 2026-09-24
4
+
5
+ ### Added
6
+
7
+ - **Per-device `deviceType` field** — a new read-only field, auto-detected from LG's own device type code, is now shown per device in the config UI; not user-editable (PR #19).
8
+ - **Automatic `deviceType` population at startup** — the plugin now actually writes `deviceType` into the saved config for every discovered device at startup (new devices get an entry created, existing entries get backfilled); previously the schema claimed this was "Auto-populated on discovery" but nothing implemented it (PR #19).
9
+ - **Per-device product identity override** — product identity (name/id shown in Apple Home) moves from a single global, AC-only override to a per-device override with type-specific defaults: AC uses 32768/"LG Air Conditioner", washer uses 32769/"LG Washer" (the washer never had a real product identity wired up before this) (PR #19).
10
+
11
+ ### Changed
12
+
13
+ - **Config UI now scoped by device type** — each device's settings only show what's relevant to its actual type: `Capabilities` and `AC Filter Control` now only appear for AC devices (`AC Filter Control` additionally requires `Supports Filter Monitoring` to be on), and `Washer Control` only appears for washer devices (PR #19).
14
+ - **`ThinQ Devices` list moved below credentials** — the device list now renders after the credential fields (password/refresh token) in the config UI instead of before them, for better visual flow (PR #19).
15
+ - **Vendor identity stays global** — vendor identity (name/id) remains a single global setting, since it applies to one Matterbridge install (PR #19).
16
+
17
+ **Upgrade note:** existing users who had customized the old shared `matterOverrideSettings.matterProductName`/`matterProductId` will see those fields disappear from the config UI on upgrade — there is no migration, this was an accepted, explicit decision — and will need to re-set a product name/ID per device if they want that customization again.
18
+
19
+ <a href="https://www.buymeacoffee.com/rinnvspktr" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
20
+
21
+ ---
22
+
23
+ ## [0.1.0-rc12] - 2026-09-24
24
+
25
+ ### Changed
26
+
27
+ - **AC and washer now pair as standalone Matter nodes** — both device endpoints are hardcoded to `server` mode, so each pairs to Apple Home as its own node with its own pairing code instead of being bridged; not user-configurable, no schema/config option (PR #18). This is experimental: whether `server` mode actually produces a per-device Apple Home power tile for the AC is unconfirmed, since properly testing it requires a real Matter Device Attestation Certificate — self-signed dev certs show "not certified" in Apple Home and may not unlock the full UI.
28
+ - **PowerTopology now uses NodeTopology** — the AC's energy-monitoring `PowerTopology` cluster switched from the `TreeTopology` feature to `NodeTopology`, matching the endpoint no longer being shared with a bridge (PR #18).
29
+ - **AC scene buttons removed** — the custom quick-access buttons that created a separate `genericSwitch` child Matter endpoint next to the AC have been removed entirely, following the same no-extra-devices-on-the-AC policy as the earlier removal of the Jet/Quiet/Energy-Save/Air-Clean/LED auxiliary toggles (PR #18).
30
+
31
+ <a href="https://www.buymeacoffee.com/rinnvspktr" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
32
+
33
+ ---
34
+
35
+ ## [0.1.0-rc11] - 2026-09-23
36
+
37
+ ### Changed
38
+
39
+ - **Removed AC auxiliary toggle child devices** — Jet, Quiet, Energy Save, Air Clean, and LED no longer create separate child switch/light devices attached to the AC in Apple Home; this is a deliberate feature removal, with no replacement control surface, so users who had these enabled will see the corresponding entities disappear from Apple Home after upgrading (PR #17).
40
+
41
+ ### Refactored
42
+
43
+ - **Removed dead `supportsDry` capability flag** — the field used to gate whether Dry opMode reported Matter SystemMode.Dry vs Cool, but that branch was already removed in the rc10 cleanup, leaving the capability field, config field, and schema toggle as inert plumbing; all three are now removed (PR #16).
44
+
45
+ <a href="https://www.buymeacoffee.com/rinnvspktr" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
46
+
47
+ ---
48
+
49
+ ## [0.1.0-rc10] - 2026-09-23
50
+
51
+ ### Added
52
+
53
+ - **LG ThinQ Washer support (v1, read-only)** — reports power, run state, and remaining time, registering as Matterbridge's LaundryWasher device type; note that Apple Home does not display the appliance device type at all, a confirmed Matterbridge/Apple platform limitation rather than a bug in this plugin, so the washer is usable today via Home Assistant, SmartThings, or other Matter controllers (PR #13).
54
+ - **Opt-in washer Stop command** — `washerControl.allowRemoteStop` (default off) lets Apple Home send a real stop/cancel command to the physical washer (PR #13).
55
+ - **AC filter life monitoring** — opt-in via `supportsFilterMonitoring` (default off), exposed as HepaFilterMonitoring; live-verified against a real device with an exact match to the official LG app (PR #15).
56
+ - **Opt-in AC filter reset command** — `acFilterControl.allowFilterReset` (default off) sends a real filter-counter reset command after you've replaced/cleaned the filter; this command has not been live-tested yet, so enable only when you are ready to verify it yourself (PR #15).
57
+
58
+ ### Fixed
59
+
60
+ - **AC mode dropdown showing unsupported values** — Dry and Fan-only opModes now report as Cool instead of SystemMode values Apple Home silently dropped, so the mode dropdown only ever shows what Apple can actually display (Off/Heat/Cool/Auto) (PR #14).
61
+
62
+ <a href="https://www.buymeacoffee.com/rinnvspktr" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
63
+
64
+ ---
65
+
66
+ ## [0.1.0-rc09] - 2026-09-20
67
+
68
+ ### Changed
69
+
70
+ - **AC is now a bridged device** — the AC registers as a normal bridged device and the hardcoded standalone "server" mode was removed, because on iOS 27 the Apple Home Energy tab (whole-home total and per-device detail) only worked in bridged mode. Note for users: remove the old standalone AC from Apple Home and pair through Matterbridge's bridge.
71
+ - **Energy measurement on the AC endpoint** — when `supportsEnergyMonitoring` is on, the electrical measurement now always lives on the AC endpoint itself (room air conditioner + power source + electrical sensor device types, with PowerTopology and ElectricalPowerMeasurement); the separate EnergyMonitor child endpoint and the `energyMonitoringPlacement` setting were removed.
72
+
73
+ ### Fixed
74
+
75
+ - **Power shown while AC is off** — power now reads 0 W when the AC is off; LG reports a floor of about 50 in that state, which was wrongly shown as real consumption.
76
+
77
+ <a href="https://www.buymeacoffee.com/rinnvspktr" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
78
+
79
+ ---
80
+
81
+ ## [0.1.0-rc08] - 2026-09-19
82
+
83
+ ### Changed
84
+
85
+ - **Power reading scale** — `airState.energy.onCurrent` is now read as raw watts (no division by 100), so reported power values are about 100 times larger than before (a running 1 hp AC shows about 900 to 1000 W); the Matter value is still written in milliwatts (PR #11).
86
+
87
+ ### Fixed
88
+
89
+ - **AC flipping On/Off in Apple Home** — LG MQTT pushes are partial and state sync treated them as full snapshots, so a missing power field read as off; state sync now writes a Matter attribute only when its source property is present in the update (PR #11).
90
+
91
+ <a href="https://www.buymeacoffee.com/rinnvspktr" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
92
+
93
+ ---
94
+
95
+ ## [0.1.0-rc07] - 2026-09-19
96
+
97
+ ### Added
98
+
99
+ - **energyMonitoringPlacement** — experimental per-device setting (`'child'` default, or `'endpoint'`) to host the electrical power measurement on the AC endpoint itself; opt-in, and the AC is not turned into an outlet (PR #10).
100
+ - **`energy` CLI command** — live power-reading diagnostics with `--device`, `--samples`, `--interval` and `--keep-alive` options.
101
+
102
+ ### Fixed
103
+
104
+ - **EnergyMonitor child endpoint** — added the PowerTopology cluster required by the electricalSensor device type, and `activePower` now starts at 0 instead of null. This changes the shape of the child, so Apple Home may need the AC removed and added again.
105
+
106
+ <a href="https://www.buymeacoffee.com/rinnvspktr" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
107
+
108
+ ---
109
+
110
+ ## [0.1.0-rc06] - 2026-09-19
111
+
112
+ ### Added
113
+
114
+ - **overrideMatterConfiguration** — configurable Matter Basic-Information identity (vendor name/id, product name/id) for the AC device, plus an optional per-device `productName` override (PR #9).
115
+
116
+ ### Fixed
117
+
118
+ - **ElectricalPowerMeasurement attribute bug** — corrected the attribute name from `power` to `activePower` and fixed the missing watts-to-milliwatts unit conversion.
119
+ - **EISDIR unhandled rejection on plugin load** — moved the MQTT certificate directory out from inside node-persist's scanned storage directory into a sibling `mqtt-certs/` directory, avoiding the crash on load.
120
+
121
+ <a href="https://www.buymeacoffee.com/rinnvspktr" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
122
+
123
+ ---
124
+
125
+ ## [0.1.0-rc05] - 2026-09-19
126
+
127
+ ### Fixed
128
+
129
+ - **ThinQ token-expiry detection** — treat LG resultCode `0102` as token-expired alongside HTTP 401, resolving AC on/off command failures caused by expired tokens not being refreshed (PR #8).
130
+
131
+ <a href="https://www.buymeacoffee.com/rinnvspktr" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
132
+
133
+ ---
134
+
135
+ ## [0.1.0-rc04] - 2026-09-19
136
+
137
+ ### Added
138
+
139
+ - **AC swing/vane control** — expose vertical/horizontal vane swing mode as a Matter control, gated per-device by capability flag.
140
+ - **AC humidity and air-quality sensors** — surface humidity and air-quality (AQI, PM2.5, PM10) readings as Matter sensors for supporting devices.
141
+ - **AC energy consumption monitoring** — report instantaneous power consumption for devices that expose it.
142
+ - **AC custom scene buttons** — optional quick-access scene buttons that switch the AC to a saved operation-mode combo with one tap.
143
+ - **AC Jet/Quiet/Energy-save/Air-clean/LED toggles** — per-device capability-gated toggles for jet mode, quiet mode, energy-save mode, air-clean mode, and LED/display light control.
144
+
145
+ <a href="https://www.buymeacoffee.com/rinnvspktr" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
146
+
147
+ ---
148
+
149
+ ## [0.1.0-rc03] - 2026-09-18
150
+
151
+ ### Added
152
+
153
+ - **AC keep-alive heartbeat** — periodic heartbeat alongside the existing polling loop keeps ThinQ AirConditioner connections alive between state refreshes, reducing stale/dropped device sessions (PR #6).
154
+
155
+ ### Changed
156
+
157
+ - **Test coverage** — closed a coverage gap to keep CI at the required 75% threshold.
158
+
159
+ <a href="https://www.buymeacoffee.com/rinnvspktr" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
160
+
161
+ ---
162
+
163
+ ## [0.1.0-rc02] - 2026-09-16
164
+
165
+ ### Added
166
+
167
+ - **ThinQ2 MQTT push support** — subscribes to ThinQ2 MQTT push notifications for near-real-time device state updates, with full test coverage.
168
+ - **Devices CLI command** — new CLI command to list all ThinQ devices discovered on the account.
169
+ - **Lifecycle debug tracing** — debug-level tracing across the ThinQ AirConditioner runtime lifecycle for easier troubleshooting.
170
+
171
+ ### Changed
172
+
173
+ - **Matterbridge dependency bump** — updated the required Matterbridge version.
174
+
175
+ <a href="https://www.buymeacoffee.com/rinnvspktr" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
176
+
177
+ ---
178
+
179
+ ## [0.1.0-rc01] - 2026-09-14
180
+
181
+ ### Added
182
+
183
+ - **Plugin bootstrap** — initial Matterbridge dynamic platform lifecycle skeleton for LG ThinQ + webOS TV integration.
184
+ - **ThinQ CLI login helper** — command-line authentication flow with session persistence for LG ThinQ accounts.
185
+ - **ThinQ AirConditioner command wiring** — device discovery, registration, and command dispatch for ThinQ AirConditioner devices.
186
+ - **Per-device AC capability flags** — configurable capability detection per AirConditioner device to gate supported features.
187
+ - **GitHub Actions workflow templates** — CI workflow scaffolding for automated builds and checks.
188
+
189
+ ### Changed
190
+
191
+ - **AirConditioner state synchronization** — polling loop applies ThinQ device snapshots to registered Matterbridge endpoints, respecting per-device capabilities.
192
+
193
+ <a href="https://www.buymeacoffee.com/rinnvspktr" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
194
+
195
+ ---
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ryan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,52 @@
1
+ import { toThinqDevice } from '../../core/domain/entities/ThinqDevice.js';
2
+ import { ThinqSession } from '../../services/thinq/session.js';
3
+ import { ThinqApiClient } from '../../services/thinq/thinqApiClient.js';
4
+ export async function cmdDevices(session, logger, dumpSnapshotDeviceId, probeFilterDeviceId) {
5
+ const { userData, country, language } = session;
6
+ const thinqSession = new ThinqSession(userData.accessToken, userData.refreshToken, userData.expiresAtEpochSeconds);
7
+ const apiClient = new ThinqApiClient(thinqSession, country, language, logger);
8
+ if (userData.userNumber) {
9
+ apiClient.setUserNumber(userData.userNumber);
10
+ }
11
+ if (probeFilterDeviceId) {
12
+ const response = await apiClient.sendCommandAndGetResponse(probeFilterDeviceId, {
13
+ ctrlKey: 'filterMngStateCtrl',
14
+ command: 'Get',
15
+ dataGetList: [
16
+ 'airState.filterMngState.useTime',
17
+ 'airState.filterMngState.remainTime',
18
+ 'airState.filterMngState.maxTime',
19
+ 'airState.filterMngState.changeDate',
20
+ 'airState.filterMngState.type',
21
+ ],
22
+ });
23
+ console.log(JSON.stringify(response, null, 2));
24
+ return;
25
+ }
26
+ const rawDevices = await apiClient.getListDevices();
27
+ const devices = rawDevices.map(toThinqDevice);
28
+ if (devices.length === 0) {
29
+ console.log('No devices found.');
30
+ return;
31
+ }
32
+ if (dumpSnapshotDeviceId) {
33
+ const device = devices.find((d) => d.id === dumpSnapshotDeviceId);
34
+ if (!device) {
35
+ console.error(`Device not found: ${dumpSnapshotDeviceId}`);
36
+ process.exitCode = 1;
37
+ return;
38
+ }
39
+ console.log(JSON.stringify(device.snapshot.raw, null, 2));
40
+ return;
41
+ }
42
+ console.log(`Found ${devices.length} device(s):\n`);
43
+ devices.forEach((device, index) => {
44
+ console.log(`${index + 1}. ${device.name}`);
45
+ console.log(` Device ID: ${device.id}`);
46
+ console.log(` Type: ${device.type}`);
47
+ console.log(` Platform Type: ${device.platformType ?? 'unknown'}`);
48
+ console.log(` Model Name: ${device.modelName}`);
49
+ console.log(` Online: ${device.online}`);
50
+ console.log('');
51
+ });
52
+ }
@@ -0,0 +1,122 @@
1
+ import { isAirConditionerDevice, toThinqDevice } from '../../core/domain/entities/ThinqDevice.js';
2
+ import { ThinqSession } from '../../services/thinq/session.js';
3
+ import { ThinqApiClient } from '../../services/thinq/thinqApiClient.js';
4
+ const ENERGY_KEY = 'airState.energy.onCurrent';
5
+ const OPERATION_KEY = 'airState.operation';
6
+ const KEEP_ALIVE_WAIT_MS = 2000;
7
+ function parsePositiveNumber(name, raw, fallback, integer) {
8
+ if (raw === undefined) {
9
+ return fallback;
10
+ }
11
+ const value = Number(raw);
12
+ if (!Number.isFinite(value) || value <= 0 || (integer && !Number.isInteger(value))) {
13
+ throw new Error(`Invalid --${name} value "${raw}": expected a positive ${integer ? 'integer' : 'number'}.`);
14
+ }
15
+ return value;
16
+ }
17
+ export function parseEnergyOptions(args) {
18
+ const deviceArg = args['device'];
19
+ return {
20
+ deviceId: deviceArg && deviceArg !== 'true' ? deviceArg : undefined,
21
+ samples: parsePositiveNumber('samples', args['samples'], 1, true),
22
+ intervalSeconds: parsePositiveNumber('interval', args['interval'], 5, false),
23
+ keepAlive: args['keep-alive'] === 'true',
24
+ };
25
+ }
26
+ function sleep(ms) {
27
+ return new Promise((resolve) => setTimeout(resolve, ms));
28
+ }
29
+ function describe(value) {
30
+ return `${JSON.stringify(value)} (typeof ${typeof value})`;
31
+ }
32
+ function listDeviceIds(devices) {
33
+ if (devices.length === 0) {
34
+ console.log(' (no devices discovered)');
35
+ return;
36
+ }
37
+ for (const device of devices) {
38
+ console.log(` ${device.id} type=${device.type} name=${device.name}`);
39
+ }
40
+ }
41
+ function printSample(device, index, total) {
42
+ const { snapshot } = device;
43
+ const raw = snapshot.raw;
44
+ console.log(`--- Sample ${index}/${total} @ ${new Date().toISOString()} ---`);
45
+ console.log(`online (device): ${device.online}`);
46
+ console.log(`online (snapshot): ${describe(raw['online'])}`);
47
+ console.log(`${OPERATION_KEY}: ${describe(raw[OPERATION_KEY])} -> isPowerOn=${snapshot.isPowerOn}`);
48
+ const keys = Object.keys(raw)
49
+ .filter((key) => {
50
+ const lower = key.toLowerCase();
51
+ return lower.startsWith('airstate.energy.') || lower.includes('energy') || lower.includes('power');
52
+ })
53
+ .sort();
54
+ console.log('Energy/power related keys:');
55
+ if (keys.length === 0) {
56
+ console.log(' (none found)');
57
+ }
58
+ for (const key of keys) {
59
+ console.log(` ${key} = ${describe(raw[key])}`);
60
+ }
61
+ console.log(`${ENERGY_KEY}:`);
62
+ if (!(ENERGY_KEY in raw)) {
63
+ console.log(' key is ABSENT from this snapshot');
64
+ }
65
+ else {
66
+ console.log(` raw value: ${describe(raw[ENERGY_KEY])}`);
67
+ }
68
+ const watts = snapshot.powerConsumptionWatts;
69
+ if (watts === undefined) {
70
+ console.log(' powerConsumptionWatts: undefined (plugin would NOT write activePower)');
71
+ }
72
+ else {
73
+ console.log(` powerConsumptionWatts: ${watts} W`);
74
+ console.log(` activePower the plugin writes: ${Math.round(watts * 1000)} mW`);
75
+ }
76
+ console.log('');
77
+ }
78
+ export async function cmdEnergy(session, options, logger) {
79
+ const { userData, country, language } = session;
80
+ const thinqSession = new ThinqSession(userData.accessToken, userData.refreshToken, userData.expiresAtEpochSeconds);
81
+ const apiClient = new ThinqApiClient(thinqSession, country, language, logger);
82
+ if (userData.userNumber) {
83
+ apiClient.setUserNumber(userData.userNumber);
84
+ }
85
+ const fetchDevices = async () => (await apiClient.getListDevices()).map(toThinqDevice);
86
+ let devices = await fetchDevices();
87
+ const target = options.deviceId
88
+ ? devices.find((device) => device.id === options.deviceId)
89
+ : devices.find(isAirConditionerDevice);
90
+ if (!target) {
91
+ console.error(options.deviceId
92
+ ? `Device "${options.deviceId}" was not found. Discovered device ids:`
93
+ : 'No AC device found. Discovered device ids:');
94
+ listDeviceIds(devices);
95
+ process.exitCode = 1;
96
+ return;
97
+ }
98
+ if (!isAirConditionerDevice(target)) {
99
+ console.warn(`Warning: device ${target.id} is type ${target.type}, not AC; continuing anyway.`);
100
+ }
101
+ console.log(`Device: ${target.name} (${target.id}) type=${target.type} model=${target.modelName || 'unknown'}`);
102
+ console.log(`Samples: ${options.samples}, interval: ${options.intervalSeconds}s, keep-alive: ${options.keepAlive ? 'yes' : 'no'}\n`);
103
+ if (options.keepAlive) {
104
+ await apiClient.sendKeepAlive(target.id);
105
+ console.log(`Keep-alive sent (allEventEnable, airState.mon.timeout=70). Waiting ${KEEP_ALIVE_WAIT_MS / 1000}s...\n`);
106
+ await sleep(KEEP_ALIVE_WAIT_MS);
107
+ devices = await fetchDevices();
108
+ }
109
+ for (let i = 1; i <= options.samples; i++) {
110
+ if (i > 1) {
111
+ await sleep(options.intervalSeconds * 1000);
112
+ devices = await fetchDevices();
113
+ }
114
+ const current = devices.find((device) => device.id === target.id);
115
+ if (!current) {
116
+ console.error(`Sample ${i}/${options.samples}: device ${target.id} is missing from the device list.`);
117
+ process.exitCode = 1;
118
+ continue;
119
+ }
120
+ printSample(current, i, options.samples);
121
+ }
122
+ }
@@ -0,0 +1,54 @@
1
+ import { AccountAuthStrategy } from '../../services/authentication/AccountAuthStrategy.js';
2
+ import { AuthenticationCoordinator } from '../../services/authentication/AuthenticationCoordinator.js';
3
+ import { TokenAuthStrategy } from '../../services/authentication/TokenAuthStrategy.js';
4
+ import { ThinqSession } from '../../services/thinq/session.js';
5
+ import { ThinqApiClient } from '../../services/thinq/thinqApiClient.js';
6
+ import { saveSession } from '../session.js';
7
+ import { maskSecret } from '../utils.js';
8
+ import { prompt } from '../utils.js';
9
+ async function buildAuthContext(type, country, language) {
10
+ if (type === 'token') {
11
+ const refreshToken = await prompt('Refresh token: ');
12
+ return {
13
+ refreshToken,
14
+ country,
15
+ language,
16
+ };
17
+ }
18
+ const username = await prompt('Username (email): ');
19
+ const password = await prompt('Password: ');
20
+ return {
21
+ username,
22
+ password,
23
+ country,
24
+ language,
25
+ };
26
+ }
27
+ export async function cmdLogin(type, country, language, logger) {
28
+ const context = await buildAuthContext(type, country, language);
29
+ const apiClient = new ThinqApiClient(new ThinqSession('', '', 0), country, language, logger);
30
+ const accountStrategy = new AccountAuthStrategy(apiClient, logger);
31
+ const tokenStrategy = new TokenAuthStrategy(apiClient, logger);
32
+ const coordinator = new AuthenticationCoordinator(accountStrategy, tokenStrategy, apiClient, logger);
33
+ const userData = await coordinator.authenticate(type, context);
34
+ if (!userData) {
35
+ console.error('Login did not complete: no session data was returned (a manual process may be required).');
36
+ process.exitCode = 1;
37
+ return;
38
+ }
39
+ const session = {
40
+ loginType: type,
41
+ country,
42
+ language,
43
+ userData,
44
+ };
45
+ saveSession(session);
46
+ console.log('Login successful. Session saved to .cli-session.json');
47
+ console.log('Session Summary:');
48
+ console.log(` Login Type: ${session.loginType}`);
49
+ console.log(` Country: ${session.country}`);
50
+ console.log(` Language: ${session.language}`);
51
+ console.log(` Access Token: ${maskSecret(session.userData.accessToken)}`);
52
+ console.log(` Refresh Token: ${maskSecret(session.userData.refreshToken)}`);
53
+ console.log(` Expires At: ${new Date(session.userData.expiresAtEpochSeconds * 1000).toISOString()}`);
54
+ }
@@ -0,0 +1,50 @@
1
+ const b = '\x1b[1m';
2
+ const c = '\x1b[36m';
3
+ const y = '\x1b[33m';
4
+ const g = '\x1b[32m';
5
+ const d = '\x1b[2m';
6
+ const r = '\x1b[0m';
7
+ const NAME_WIDTH = 12;
8
+ function row(name, args, desc) {
9
+ const paddedName = name.padEnd(NAME_WIDTH);
10
+ const paddedArgs = args.padEnd(40);
11
+ return ` ${g}${paddedName}${r}${d}${paddedArgs}${r}${desc}`;
12
+ }
13
+ export const HELP_TEXT = `
14
+ ${b}Usage:${r}
15
+ npm run cli -- --command ${c}<command>${r} [options]
16
+
17
+ ${b}Options:${r}
18
+ ${y}--type${r} account|token (default: account)
19
+ ${y}--country${r} Country code (default: US)
20
+ ${y}--language${r} Language code (default: en-US)
21
+ ${y}--help${r} Show this help message
22
+ ${y}--debug${r} Enable debug logging
23
+
24
+ ${b}Devices options:${r}
25
+ ${y}--dump-snapshot${r} Device id; prints its full raw ThinQ snapshot JSON instead of the device list
26
+ ${y}--probe-filter${r} Device id; sends a filterMngStateCtrl Get probe and prints the raw response JSON
27
+
28
+ ${b}Energy options:${r}
29
+ ${y}--device${r} Device id (default: first AC device)
30
+ ${y}--samples${r} Number of snapshots to fetch (default: 1)
31
+ ${y}--interval${r} Seconds between samples (default: 5)
32
+ ${y}--keep-alive${r} Send the plugin's keep-alive first, wait 2s, then sample
33
+
34
+ ${b}Commands:${r}
35
+ ${row('login', '', 'Authenticate with LG ThinQ')}
36
+ ${row('devices', '', 'List all discovered ThinQ devices (requires a prior login)')}
37
+ ${row('energy', '[energy options]', 'Print raw AC power/energy snapshot fields (requires a prior login)')}
38
+ ${row('help', '', 'Show this help message')}
39
+
40
+ ${b}Examples:${r}
41
+ npm run cli -- ${y}--help${r}
42
+ npm run cli -- --command ${g}login${r}
43
+ npm run cli -- --command ${g}login${r} --type account --country US --language en-US
44
+ npm run cli -- --command ${g}login${r} --type token
45
+ npm run cli -- --command ${g}devices${r}
46
+ npm run cli -- --command ${g}devices${r} --dump-snapshot <deviceId>
47
+ npm run cli -- --command ${g}devices${r} --probe-filter <deviceId>
48
+ npm run cli -- --command ${g}energy${r}
49
+ npm run cli -- --command ${g}energy${r} --samples 5 --interval 10 --keep-alive
50
+ `;
@@ -0,0 +1,56 @@
1
+ import { AnsiLogger } from 'matterbridge/logger';
2
+ import { cmdDevices } from './commands/devices.js';
3
+ import { cmdEnergy, parseEnergyOptions } from './commands/energy.js';
4
+ import { cmdLogin } from './commands/login.js';
5
+ import { HELP_TEXT } from './help.js';
6
+ import { loadSession } from './session.js';
7
+ import { parseArgs } from './utils.js';
8
+ export async function main() {
9
+ const args = parseArgs(process.argv.slice(2));
10
+ const command = args['command'];
11
+ const help = args['help'] === 'true';
12
+ if (!command || command === 'help' || help) {
13
+ console.log(HELP_TEXT);
14
+ process.exit(command || help ? 0 : 1);
15
+ }
16
+ const debug = args['debug'] === 'true';
17
+ const logger = AnsiLogger.create({ logName: 'CLI', logLevel: debug ? "debug" : "warn" });
18
+ try {
19
+ if (command === 'login') {
20
+ const type = (args['type'] || 'account');
21
+ const country = args['country'] || 'US';
22
+ const language = args['language'] || 'en-US';
23
+ await cmdLogin(type, country, language, logger);
24
+ return;
25
+ }
26
+ if (command === 'devices') {
27
+ const session = loadSession();
28
+ if (!session) {
29
+ console.error('No session found. Run `--command login` first.');
30
+ process.exitCode = 1;
31
+ return;
32
+ }
33
+ const dumpSnapshotDeviceId = args['dump-snapshot'];
34
+ const probeFilterDeviceId = args['probe-filter'];
35
+ await cmdDevices(session, logger, dumpSnapshotDeviceId, probeFilterDeviceId);
36
+ return;
37
+ }
38
+ if (command === 'energy') {
39
+ const options = parseEnergyOptions(args);
40
+ const session = loadSession();
41
+ if (!session) {
42
+ console.error('No session found. Run `--command login` first.');
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+ await cmdEnergy(session, options, logger);
47
+ return;
48
+ }
49
+ console.error(`Unknown command: ${command}\n\n${HELP_TEXT}`);
50
+ process.exit(1);
51
+ }
52
+ catch (err) {
53
+ console.error('Error:', err instanceof Error ? err.message : String(err));
54
+ process.exit(1);
55
+ }
56
+ }
@@ -0,0 +1,13 @@
1
+ import fs from 'node:fs';
2
+ import { SESSION_FILE } from './types.js';
3
+ export function saveSession(session) {
4
+ fs.writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2));
5
+ }
6
+ export function loadSession() {
7
+ try {
8
+ return JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'));
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
@@ -0,0 +1 @@
1
+ export const SESSION_FILE = '.cli-session.json';
@@ -0,0 +1,29 @@
1
+ import readline from 'node:readline';
2
+ export { maskSecret } from '../utils/mask.js';
3
+ export function parseArgs(args) {
4
+ const result = {};
5
+ for (let i = 0; i < args.length; i++) {
6
+ const key = args[i];
7
+ if (!key.startsWith('--'))
8
+ continue;
9
+ const name = key.slice(2);
10
+ const next = args[i + 1];
11
+ if (next !== undefined && !next.startsWith('--')) {
12
+ result[name] = next;
13
+ i++;
14
+ }
15
+ else {
16
+ result[name] = 'true';
17
+ }
18
+ }
19
+ return result;
20
+ }
21
+ export async function prompt(question) {
22
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
23
+ return new Promise((resolve) => {
24
+ rl.question(question, (answer) => {
25
+ rl.close();
26
+ resolve(answer.trim());
27
+ });
28
+ });
29
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,2 @@
1
+ import { main } from './cli/main.js';
2
+ main();
@@ -0,0 +1 @@
1
+ export const UNREGISTER_DEVICES_DELAY_MS = 500;
@@ -0,0 +1,43 @@
1
+ import { ThinqSnapshot } from '../value-objects/ThinqSnapshot.js';
2
+ const DEVICE_ID_PATTERN = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
3
+ const DEVICE_TYPE_NAMES = {
4
+ 101: 'REFRIGERATOR',
5
+ 102: 'KIMCHI_REFRIGERATOR',
6
+ 103: 'WATER_PURIFIER',
7
+ 201: 'WASHER',
8
+ 221: 'WASHER_NEW',
9
+ 222: 'WASH_TOWER',
10
+ 223: 'WASH_TOWER_2',
11
+ 202: 'DRYER',
12
+ 203: 'STYLER',
13
+ 204: 'DISHWASHER',
14
+ 301: 'OVEN',
15
+ 302: 'MICROWAVE',
16
+ 303: 'COOKTOP',
17
+ 304: 'HOOD',
18
+ 401: 'AC',
19
+ 402: 'AIR_PURIFIER',
20
+ 403: 'DEHUMIDIFIER',
21
+ 410: 'AERO_TOWER',
22
+ };
23
+ export function isValidThinqDeviceId(id) {
24
+ return typeof id === 'string' && DEVICE_ID_PATTERN.test(id);
25
+ }
26
+ export function isAirConditionerDevice(device) {
27
+ return device.type === 'AC';
28
+ }
29
+ export function isWasherDevice(device) {
30
+ return device.type === 'WASHER';
31
+ }
32
+ export function toThinqDevice(data) {
33
+ return {
34
+ id: data.deviceId,
35
+ name: data.alias,
36
+ type: DEVICE_TYPE_NAMES[data.deviceType] ?? String(data.deviceType),
37
+ modelName: data.modelName ?? data.modemInfo?.modelName ?? data.manufacture?.manufactureModel ?? '',
38
+ platformType: data.platformType,
39
+ online: data.online ?? data.snapshot.online ?? false,
40
+ snapshot: new ThinqSnapshot(data.snapshot),
41
+ modelJsonUri: data.modelJsonUri,
42
+ };
43
+ }