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,190 @@
1
+ import { FanControl, Thermostat } from 'matterbridge/matter/clusters';
2
+ import { isValidNumber } from 'matterbridge/utils';
3
+ export const THINQ_FAN_SPEED_AUTO = 8;
4
+ export const THINQ_FAN_SPEED_LOW = 2;
5
+ export const THINQ_FAN_SPEED_MEDIUM = 4;
6
+ export const THINQ_FAN_SPEED_HIGH = 6;
7
+ const THINQ_OPERATION_ON = 1;
8
+ const THINQ_OPERATION_OFF = 0;
9
+ const THINQ_TEMPERATURE_STEP_CELSIUS = 0.5;
10
+ function withErrorHandling(commandName, logger, handler) {
11
+ return async () => {
12
+ try {
13
+ await handler();
14
+ }
15
+ catch (error) {
16
+ logger.error(`ThinQ AirConditioner command '${commandName}' failed: ${error instanceof Error ? error.message : String(error)}`);
17
+ throw error;
18
+ }
19
+ };
20
+ }
21
+ function roundToStep(value, step) {
22
+ return Math.round(value / step) * step;
23
+ }
24
+ function revertSetpointOnFailure(airConditioner, attribute, oldValue) {
25
+ return () => {
26
+ airConditioner.updateAttribute(Thermostat.id, attribute, oldValue, airConditioner.log).catch(() => {
27
+ });
28
+ };
29
+ }
30
+ function mapPercentToWindStrength(percent) {
31
+ if (percent === 0) {
32
+ return undefined;
33
+ }
34
+ if (percent <= 33) {
35
+ return THINQ_FAN_SPEED_LOW;
36
+ }
37
+ if (percent <= 66) {
38
+ return THINQ_FAN_SPEED_MEDIUM;
39
+ }
40
+ return THINQ_FAN_SPEED_HIGH;
41
+ }
42
+ function mapFanModeToWindStrength(fanMode) {
43
+ switch (fanMode) {
44
+ case FanControl.FanMode.Auto:
45
+ return THINQ_FAN_SPEED_AUTO;
46
+ case FanControl.FanMode.Low:
47
+ return THINQ_FAN_SPEED_LOW;
48
+ case FanControl.FanMode.Medium:
49
+ return THINQ_FAN_SPEED_MEDIUM;
50
+ case FanControl.FanMode.High:
51
+ return THINQ_FAN_SPEED_HIGH;
52
+ default:
53
+ return undefined;
54
+ }
55
+ }
56
+ export function registerAirConditionerCommandHandlers(airConditioner, device, apiClient, logger, capabilities) {
57
+ airConditioner.addCommandHandler('on', withErrorHandling('on', logger, async () => {
58
+ logger.debug(`ThinQ AirConditioner ${device.id}: 'on' command handler invoked, capabilities=${JSON.stringify(capabilities)}`);
59
+ await apiClient.sendCommand(device.id, {
60
+ command: 'Operation',
61
+ dataKey: 'airState.operation',
62
+ dataValue: THINQ_OPERATION_ON,
63
+ });
64
+ }));
65
+ airConditioner.addCommandHandler('off', withErrorHandling('off', logger, async () => {
66
+ logger.debug(`ThinQ AirConditioner ${device.id}: 'off' command handler invoked, capabilities=${JSON.stringify(capabilities)}`);
67
+ await apiClient.sendCommand(device.id, {
68
+ command: 'Operation',
69
+ dataKey: 'airState.operation',
70
+ dataValue: THINQ_OPERATION_OFF,
71
+ });
72
+ }));
73
+ airConditioner.subscribeAttribute(Thermostat.id, 'occupiedCoolingSetpoint', (newValue, oldValue, context) => {
74
+ if (context.fabric === undefined) {
75
+ return;
76
+ }
77
+ logger.debug(`ThinQ AirConditioner ${device.id}: 'occupiedCoolingSetpoint' command handler invoked, newValue=${newValue}, capabilities=${JSON.stringify(capabilities)}`);
78
+ void withErrorHandling('occupiedCoolingSetpoint', logger, async () => {
79
+ await apiClient.sendCommand(device.id, {
80
+ dataKey: 'airState.tempState.target',
81
+ dataValue: roundToStep(newValue / 100, THINQ_TEMPERATURE_STEP_CELSIUS),
82
+ });
83
+ })().catch(revertSetpointOnFailure(airConditioner, 'occupiedCoolingSetpoint', oldValue));
84
+ }, airConditioner.log);
85
+ if (capabilities.supportsHeat) {
86
+ airConditioner.subscribeAttribute(Thermostat.id, 'occupiedHeatingSetpoint', (newValue, oldValue, context) => {
87
+ if (context.fabric === undefined) {
88
+ return;
89
+ }
90
+ logger.debug(`ThinQ AirConditioner ${device.id}: 'occupiedHeatingSetpoint' command handler invoked, newValue=${newValue}, capabilities=${JSON.stringify(capabilities)}`);
91
+ void withErrorHandling('occupiedHeatingSetpoint', logger, async () => {
92
+ await apiClient.sendCommand(device.id, {
93
+ dataKey: 'airState.tempState.target',
94
+ dataValue: roundToStep(newValue / 100, THINQ_TEMPERATURE_STEP_CELSIUS),
95
+ });
96
+ })().catch(revertSetpointOnFailure(airConditioner, 'occupiedHeatingSetpoint', oldValue));
97
+ }, airConditioner.log);
98
+ }
99
+ airConditioner.subscribeAttribute(FanControl, 'percentSetting', (newValue, _oldValue, context) => {
100
+ if (context.fabric === undefined) {
101
+ return;
102
+ }
103
+ logger.debug(`ThinQ AirConditioner ${device.id}: 'percentSetting' command handler invoked, newValue=${newValue}, capabilities=${JSON.stringify(capabilities)}`);
104
+ if (!capabilities.supportsFanSpeedControl) {
105
+ logger.debug(`ThinQ AirConditioner ${device.id}: ignoring percentSetting change — fan speed control not supported per configured capabilities.`);
106
+ return;
107
+ }
108
+ if (newValue === null || !isValidNumber(newValue, 0, 100)) {
109
+ return;
110
+ }
111
+ const windStrength = mapPercentToWindStrength(newValue);
112
+ if (windStrength === undefined) {
113
+ logger.debug(`ThinQ AirConditioner ${device.id}: ignoring percentSetting=0 (ambiguous, no ThinQ equivalent).`);
114
+ return;
115
+ }
116
+ void withErrorHandling('percentSetting', logger, async () => {
117
+ await apiClient.sendCommand(device.id, {
118
+ dataKey: 'airState.windStrength',
119
+ dataValue: windStrength,
120
+ });
121
+ })();
122
+ }, airConditioner.log);
123
+ airConditioner.subscribeAttribute(FanControl, 'fanMode', (newValue, _oldValue, context) => {
124
+ if (context.fabric === undefined) {
125
+ return;
126
+ }
127
+ logger.debug(`ThinQ AirConditioner ${device.id}: 'fanMode' command handler invoked, newValue=${FanControl.FanMode[newValue]}, capabilities=${JSON.stringify(capabilities)}`);
128
+ if (!capabilities.supportsFanSpeedControl) {
129
+ logger.debug(`ThinQ AirConditioner ${device.id}: ignoring fanMode change — fan speed control not supported per configured capabilities.`);
130
+ return;
131
+ }
132
+ const windStrength = mapFanModeToWindStrength(newValue);
133
+ if (windStrength === undefined) {
134
+ logger.debug(`ThinQ AirConditioner ${device.id}: ignoring fanMode=${FanControl.FanMode[newValue]} (no ThinQ equivalent).`);
135
+ return;
136
+ }
137
+ void withErrorHandling('fanMode', logger, async () => {
138
+ await apiClient.sendCommand(device.id, {
139
+ dataKey: 'airState.windStrength',
140
+ dataValue: windStrength,
141
+ });
142
+ })();
143
+ }, airConditioner.log);
144
+ if (capabilities.supportsSwingMode) {
145
+ airConditioner.subscribeAttribute(FanControl, 'rockSetting', (newValue, oldValue, context) => {
146
+ if (context.fabric === undefined) {
147
+ return;
148
+ }
149
+ const newRock = newValue;
150
+ const oldRock = oldValue;
151
+ logger.debug(`ThinQ AirConditioner ${device.id}: 'rockSetting' command handler invoked, newValue=${JSON.stringify(newRock)}, capabilities=${JSON.stringify(capabilities)}`);
152
+ if (!newRock || !oldRock) {
153
+ return;
154
+ }
155
+ const verticalChanged = newRock.rockUpDown !== oldRock.rockUpDown;
156
+ const horizontalChanged = newRock.rockLeftRight !== oldRock.rockLeftRight;
157
+ if (!verticalChanged && !horizontalChanged) {
158
+ return;
159
+ }
160
+ void withErrorHandling('rockSetting', logger, async () => {
161
+ if (verticalChanged && horizontalChanged && newRock.rockUpDown && newRock.rockLeftRight) {
162
+ await apiClient.sendCommand(device.id, {
163
+ dataKey: null,
164
+ dataValue: null,
165
+ command: 'Set',
166
+ ctrlKey: 'favoriteCtrl',
167
+ dataSetList: {
168
+ 'airState.wDir.vStep': '100',
169
+ 'airState.wDir.hStep': '100',
170
+ },
171
+ });
172
+ }
173
+ else {
174
+ if (verticalChanged) {
175
+ await apiClient.sendCommand(device.id, {
176
+ dataKey: 'airState.wDir.vStep',
177
+ dataValue: newRock.rockUpDown ? '100' : '0',
178
+ });
179
+ }
180
+ if (horizontalChanged) {
181
+ await apiClient.sendCommand(device.id, {
182
+ dataKey: 'airState.wDir.hStep',
183
+ dataValue: newRock.rockLeftRight ? '100' : '0',
184
+ });
185
+ }
186
+ }
187
+ })();
188
+ }, airConditioner.log);
189
+ }
190
+ }
@@ -0,0 +1,53 @@
1
+ import { airQualitySensor, electricalSensor, humiditySensor, MatterbridgeEndpoint, powerSource, roomAirConditioner, } from 'matterbridge';
2
+ import { AirQuality, FanControl, PowerTopology } from 'matterbridge/matter/clusters';
3
+ export function buildAirConditionerEndpoint(device, capabilities, setpoints, initialFanMode, options) {
4
+ const { currentTemperature, targetTemperature, minHeatSetpointLimitCelsius, maxHeatSetpointLimitCelsius, minCoolSetpointLimitCelsius, maxCoolSetpointLimitCelsius, } = setpoints;
5
+ const endpoint = new MatterbridgeEndpoint(capabilities.supportsEnergyMonitoring
6
+ ? [roomAirConditioner, powerSource, electricalSensor]
7
+ : [roomAirConditioner, powerSource], {
8
+ id: `${device.name.replaceAll(' ', '')}-${device.id.replaceAll(' ', '')}`,
9
+ })
10
+ .createDefaultIdentifyClusterServer()
11
+ .createDefaultBasicInformationClusterServer(device.name, device.id, options?.vendorId ?? 0xfff1, options?.vendorName ?? 'Matterbridge', options?.productId ?? 0x8000, options?.productName ?? 'Matterbridge Air Conditioner')
12
+ .createDefaultPowerSourceWiredClusterServer()
13
+ .createDeadFrontOnOffClusterServer(true);
14
+ if (capabilities.supportsHeat) {
15
+ endpoint.createDefaultThermostatClusterServer(currentTemperature, targetTemperature, targetTemperature, 1, minHeatSetpointLimitCelsius, maxHeatSetpointLimitCelsius, minCoolSetpointLimitCelsius, maxCoolSetpointLimitCelsius);
16
+ }
17
+ else {
18
+ endpoint.createDefaultCoolingThermostatClusterServer(currentTemperature, targetTemperature, minCoolSetpointLimitCelsius, maxCoolSetpointLimitCelsius);
19
+ }
20
+ endpoint.createDefaultThermostatUserInterfaceConfigurationClusterServer();
21
+ if (capabilities.supportsFanSpeedControl && capabilities.supportsSwingMode) {
22
+ endpoint.createCompleteFanControlClusterServer(initialFanMode, FanControl.FanModeSequence.OffLowMedHighAuto, 0, 0, undefined, undefined, undefined, { rockLeftRight: true, rockUpDown: true, rockRound: true }, { rockLeftRight: false, rockUpDown: false, rockRound: false });
23
+ }
24
+ else if (capabilities.supportsFanSpeedControl) {
25
+ endpoint.createDefaultFanControlClusterServer(initialFanMode, FanControl.FanModeSequence.OffLowMedHighAuto, 0, 0);
26
+ }
27
+ else {
28
+ endpoint.createOnOffFanControlClusterServer(initialFanMode);
29
+ }
30
+ if (capabilities.supportsHumiditySensor) {
31
+ endpoint
32
+ .addChildDeviceType('HumiditySensor', [humiditySensor])
33
+ .createDefaultIdentifyClusterServer()
34
+ .createDefaultRelativeHumidityMeasurementClusterServer(0);
35
+ }
36
+ if (capabilities.supportsAirQualitySensor) {
37
+ endpoint
38
+ .addChildDeviceType('AirQualitySensor', [airQualitySensor])
39
+ .createDefaultIdentifyClusterServer()
40
+ .createDefaultAirQualityClusterServer(AirQuality.AirQualityEnum.Unknown)
41
+ .createDefaultPm25ConcentrationMeasurementClusterServer()
42
+ .createDefaultPm10ConcentrationMeasurementClusterServer();
43
+ }
44
+ if (capabilities.supportsEnergyMonitoring) {
45
+ endpoint
46
+ .createDefaultPowerTopologyClusterServer(PowerTopology.Feature.NodeTopology)
47
+ .createDefaultElectricalPowerMeasurementClusterServer(null, null, 0, null);
48
+ }
49
+ if (capabilities.supportsFilterMonitoring) {
50
+ endpoint.createDefaultHepaFilterMonitoringClusterServer();
51
+ }
52
+ return endpoint;
53
+ }
@@ -0,0 +1,54 @@
1
+ import { ThinqFilterState } from '../../core/domain/value-objects/ThinqFilterState.js';
2
+ const KEY_USE_TIME = 'airState.filterMngState.useTime';
3
+ const KEY_REMAIN_TIME = 'airState.filterMngState.remainTime';
4
+ const KEY_MAX_TIME = 'airState.filterMngState.maxTime';
5
+ const KEY_CHANGE_DATE = 'airState.filterMngState.changeDate';
6
+ export function resolveFilterResetPayload(filterState, now = new Date()) {
7
+ return {
8
+ [KEY_USE_TIME]: 0,
9
+ [KEY_REMAIN_TIME]: filterState.ratedMaxTimeHours,
10
+ [KEY_MAX_TIME]: filterState.ratedMaxTimeHours,
11
+ [KEY_CHANGE_DATE]: formatChangeDate(now),
12
+ };
13
+ }
14
+ function formatChangeDate(date) {
15
+ const year = date.getUTCFullYear();
16
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0');
17
+ const day = String(date.getUTCDate()).padStart(2, '0');
18
+ return Number(`${year}${month}${day}`);
19
+ }
20
+ export function registerFilterResetCommandHandler(airConditioner, device, apiClient, logger, capabilities, filterResetControl) {
21
+ if (!capabilities.supportsFilterMonitoring) {
22
+ return;
23
+ }
24
+ airConditioner.addCommandHandler('resetCondition', async () => {
25
+ if (!filterResetControl.allowFilterReset) {
26
+ logger.warn(`ThinQ AirConditioner filter reset is disabled by configuration (deviceId=${device.id})`);
27
+ throw new Error('ThinQ AirConditioner filter reset is disabled by configuration.');
28
+ }
29
+ let filterState;
30
+ try {
31
+ filterState = ThinqFilterState.fromRaw(await apiClient.getFilterState(device.id));
32
+ }
33
+ catch (error) {
34
+ logger.error(`ThinQ AirConditioner filter reset failed to read current filter state: ${error instanceof Error ? error.message : String(error)} (deviceId=${device.id})`);
35
+ throw error;
36
+ }
37
+ if (!filterState) {
38
+ logger.warn(`ThinQ AirConditioner filter reset: no current filter data available, refusing to reset (deviceId=${device.id})`);
39
+ throw new Error('ThinQ AirConditioner filter reset: no current filter data available for this device.');
40
+ }
41
+ try {
42
+ await apiClient.sendCommand(device.id, {
43
+ ctrlKey: 'filterMngStateCtrl',
44
+ command: 'Set',
45
+ dataSetList: resolveFilterResetPayload(filterState),
46
+ });
47
+ logger.info(`ThinQ AirConditioner filter reset succeeded (deviceId=${device.id})`);
48
+ }
49
+ catch (error) {
50
+ logger.error(`ThinQ AirConditioner filter reset command failed: ${error instanceof Error ? error.message : String(error)} (deviceId=${device.id})`);
51
+ throw error;
52
+ }
53
+ });
54
+ }
@@ -0,0 +1,19 @@
1
+ import { HepaFilterMonitoring, ResourceMonitoring } from 'matterbridge/matter/clusters';
2
+ const FILTER_WARNING_THRESHOLD_PERCENT = 10;
3
+ export function resolveFilterChangeIndication(remainingPercent) {
4
+ if (remainingPercent <= 0) {
5
+ return ResourceMonitoring.ChangeIndication.Critical;
6
+ }
7
+ if (remainingPercent <= FILTER_WARNING_THRESHOLD_PERCENT) {
8
+ return ResourceMonitoring.ChangeIndication.Warning;
9
+ }
10
+ return ResourceMonitoring.ChangeIndication.Ok;
11
+ }
12
+ export async function applyThinqFilterStateToAirConditioner(airConditioner, filterState, logger) {
13
+ if (!filterState) {
14
+ logger.debug('ThinQ filter state: no data, skipping HepaFilterMonitoring update.');
15
+ return;
16
+ }
17
+ await airConditioner.updateAttribute(HepaFilterMonitoring.id, 'condition', filterState.remainingPercent, logger);
18
+ await airConditioner.updateAttribute(HepaFilterMonitoring.id, 'changeIndication', resolveFilterChangeIndication(filterState.remainingPercent), logger);
19
+ }
@@ -0,0 +1,205 @@
1
+ import { AirQuality, ElectricalPowerMeasurement, FanControl, OnOff, Pm10ConcentrationMeasurement, Pm25ConcentrationMeasurement, RelativeHumidityMeasurement, TemperatureMeasurement, Thermostat, } from 'matterbridge/matter/clusters';
2
+ import { THINQ_FAN_SPEED_AUTO, THINQ_FAN_SPEED_LOW, THINQ_FAN_SPEED_MEDIUM, } from './thinqAirConditionerCommandHandlers.js';
3
+ import { mapWindStrengthToFanMode, mapWindStrengthToFixedFanMode } from './thinqDeviceConfigurator.js';
4
+ const THINQ_OP_MODE_COOL = 0;
5
+ const THINQ_OP_MODE_DRY = 1;
6
+ const THINQ_OP_MODE_FAN = 2;
7
+ const THINQ_OP_MODE_HEAT = 4;
8
+ const THINQ_OP_MODE_AIR_CLEAN = 5;
9
+ const THINQ_OP_MODE_AUTO = 6;
10
+ const WIND_STRENGTH_LOW_PERCENT = 20;
11
+ const WIND_STRENGTH_MEDIUM_PERCENT = 50;
12
+ const WIND_STRENGTH_HIGH_PERCENT = 90;
13
+ const KEY_OPERATION = 'airState.operation';
14
+ const KEY_OP_MODE = 'airState.opMode';
15
+ const KEY_TEMP_CURRENT = 'airState.tempState.current';
16
+ const KEY_TEMP_TARGET = 'airState.tempState.target';
17
+ const KEY_WIND_STRENGTH = 'airState.windStrength';
18
+ const KEY_SWING_VERTICAL = 'airState.wDir.vStep';
19
+ const KEY_SWING_HORIZONTAL = 'airState.wDir.hStep';
20
+ export function mapWindStrengthToPercent(windStrength) {
21
+ if (windStrength === undefined || windStrength === THINQ_FAN_SPEED_AUTO) {
22
+ return undefined;
23
+ }
24
+ if (windStrength <= THINQ_FAN_SPEED_LOW) {
25
+ return WIND_STRENGTH_LOW_PERCENT;
26
+ }
27
+ if (windStrength <= THINQ_FAN_SPEED_MEDIUM) {
28
+ return WIND_STRENGTH_MEDIUM_PERCENT;
29
+ }
30
+ return WIND_STRENGTH_HIGH_PERCENT;
31
+ }
32
+ export function mapOperationModeToSystemMode(operationMode, isPowerOn, capabilities) {
33
+ if (!isPowerOn) {
34
+ return Thermostat.SystemMode.Off;
35
+ }
36
+ switch (operationMode) {
37
+ case THINQ_OP_MODE_AUTO:
38
+ return capabilities.supportsHeat ? Thermostat.SystemMode.Auto : Thermostat.SystemMode.Cool;
39
+ case THINQ_OP_MODE_COOL:
40
+ return Thermostat.SystemMode.Cool;
41
+ case THINQ_OP_MODE_HEAT:
42
+ return capabilities.supportsHeat ? Thermostat.SystemMode.Heat : Thermostat.SystemMode.Cool;
43
+ case THINQ_OP_MODE_FAN:
44
+ return Thermostat.SystemMode.Cool;
45
+ case THINQ_OP_MODE_DRY:
46
+ return Thermostat.SystemMode.Cool;
47
+ case THINQ_OP_MODE_AIR_CLEAN:
48
+ default:
49
+ return capabilities.supportsHeat ? Thermostat.SystemMode.Auto : Thermostat.SystemMode.Cool;
50
+ }
51
+ }
52
+ function resolveSystemModeUpdate(airConditioner, snapshot, capabilities) {
53
+ if (!snapshot.has(KEY_OPERATION)) {
54
+ if (!snapshot.has(KEY_OP_MODE)) {
55
+ return undefined;
56
+ }
57
+ const currentPowerOn = airConditioner.getAttribute(OnOff.id, 'onOff') === true;
58
+ if (!currentPowerOn) {
59
+ return undefined;
60
+ }
61
+ return mapOperationModeToSystemMode(snapshot.operationMode, true, capabilities);
62
+ }
63
+ const isPowerOn = snapshot.isPowerOn;
64
+ if (isPowerOn && !snapshot.has(KEY_OP_MODE)) {
65
+ return undefined;
66
+ }
67
+ return mapOperationModeToSystemMode(snapshot.operationMode, isPowerOn, capabilities);
68
+ }
69
+ function isKnownOff(airConditioner, snapshot) {
70
+ if (snapshot.has(KEY_OPERATION)) {
71
+ return !snapshot.isPowerOn;
72
+ }
73
+ return airConditioner.getAttribute(OnOff.id, 'onOff') === false;
74
+ }
75
+ function resolveRockSetting(airConditioner, snapshot) {
76
+ const hasVertical = snapshot.has(KEY_SWING_VERTICAL);
77
+ const hasHorizontal = snapshot.has(KEY_SWING_HORIZONTAL);
78
+ if (!hasVertical && !hasHorizontal) {
79
+ return undefined;
80
+ }
81
+ let verticalOn = snapshot.isVerticalSwingOn;
82
+ let horizontalOn = snapshot.isHorizontalSwingOn;
83
+ if (!hasVertical) {
84
+ const currentRockSetting = airConditioner.getAttribute(FanControl.id, 'rockSetting');
85
+ verticalOn = currentRockSetting?.rockUpDown ?? false;
86
+ }
87
+ if (!hasHorizontal) {
88
+ const currentRockSetting = airConditioner.getAttribute(FanControl.id, 'rockSetting');
89
+ horizontalOn = currentRockSetting?.rockLeftRight ?? false;
90
+ }
91
+ return {
92
+ rockLeftRight: horizontalOn,
93
+ rockUpDown: verticalOn,
94
+ rockRound: verticalOn && horizontalOn,
95
+ };
96
+ }
97
+ export async function applyThinqSnapshotToAirConditioner(airConditioner, snapshot, capabilities, logger) {
98
+ const deviceId = airConditioner.serialNumber ?? airConditioner.uniqueId ?? 'unknown';
99
+ const hasPower = snapshot.has(KEY_OPERATION);
100
+ const systemModeUpdate = resolveSystemModeUpdate(airConditioner, snapshot, capabilities);
101
+ const rockSetting = capabilities.supportsFanSpeedControl && capabilities.supportsSwingMode
102
+ ? resolveRockSetting(airConditioner, snapshot)
103
+ : undefined;
104
+ const attributesToPush = [];
105
+ const attributesToSkip = [];
106
+ if (hasPower) {
107
+ attributesToPush.push('power');
108
+ }
109
+ else {
110
+ attributesToSkip.push('power');
111
+ }
112
+ if (systemModeUpdate !== undefined) {
113
+ attributesToPush.push('systemMode');
114
+ }
115
+ else {
116
+ attributesToSkip.push('systemMode');
117
+ }
118
+ if (snapshot.currentTemperatureCelsius !== undefined) {
119
+ attributesToPush.push('currentTemp');
120
+ }
121
+ else if (snapshot.has(KEY_TEMP_CURRENT)) {
122
+ attributesToSkip.push('currentTemp');
123
+ }
124
+ if (snapshot.targetTemperatureCelsius !== undefined) {
125
+ attributesToPush.push('targetTemp');
126
+ }
127
+ else if (snapshot.has(KEY_TEMP_TARGET)) {
128
+ attributesToSkip.push('targetTemp');
129
+ }
130
+ if (snapshot.has(KEY_WIND_STRENGTH)) {
131
+ attributesToPush.push('fanSpeed');
132
+ }
133
+ else if (capabilities.supportsFanSpeedControl) {
134
+ attributesToSkip.push('fanSpeed');
135
+ }
136
+ logger.debug(`applyThinqSnapshotToAirConditioner: entry for deviceId=${deviceId}, pushing ${attributesToPush.length} attributes: ${attributesToPush.join(', ')}`);
137
+ if (attributesToSkip.length > 0) {
138
+ logger.debug(`applyThinqSnapshotToAirConditioner: skipped (source key absent): ${attributesToSkip.join(', ')}`);
139
+ }
140
+ if (hasPower) {
141
+ await airConditioner.updateAttribute(OnOff.id, 'onOff', snapshot.isPowerOn, logger);
142
+ }
143
+ const currentTemperatureCelsius = snapshot.currentTemperatureCelsius;
144
+ if (currentTemperatureCelsius !== undefined) {
145
+ await airConditioner.updateAttribute(TemperatureMeasurement.id, 'measuredValue', currentTemperatureCelsius * 100, logger);
146
+ await airConditioner.updateAttribute(Thermostat.id, 'localTemperature', currentTemperatureCelsius * 100, logger);
147
+ }
148
+ const targetTemperatureCelsius = snapshot.targetTemperatureCelsius;
149
+ if (targetTemperatureCelsius !== undefined) {
150
+ await airConditioner.updateAttribute(Thermostat.id, 'occupiedCoolingSetpoint', targetTemperatureCelsius * 100, logger);
151
+ if (capabilities.supportsHeat) {
152
+ await airConditioner.updateAttribute(Thermostat.id, 'occupiedHeatingSetpoint', targetTemperatureCelsius * 100, logger);
153
+ }
154
+ }
155
+ if (systemModeUpdate !== undefined) {
156
+ await airConditioner.updateAttribute(Thermostat.id, 'systemMode', systemModeUpdate, logger);
157
+ }
158
+ if (snapshot.has(KEY_WIND_STRENGTH)) {
159
+ if (capabilities.supportsFanSpeedControl) {
160
+ await airConditioner.updateAttribute(FanControl.id, 'fanMode', mapWindStrengthToFanMode(snapshot.windStrength), logger);
161
+ const percentCurrent = mapWindStrengthToPercent(snapshot.windStrength);
162
+ if (percentCurrent !== undefined) {
163
+ await airConditioner.updateAttribute(FanControl.id, 'percentCurrent', percentCurrent, logger);
164
+ }
165
+ }
166
+ else {
167
+ await airConditioner.updateAttribute(FanControl.id, 'fanMode', mapWindStrengthToFixedFanMode(snapshot.windStrength), logger);
168
+ }
169
+ }
170
+ if (capabilities.supportsFanSpeedControl && capabilities.supportsSwingMode && rockSetting !== undefined) {
171
+ await airConditioner.updateAttribute(FanControl.id, 'rockSetting', rockSetting, logger);
172
+ }
173
+ if (capabilities.supportsHumiditySensor) {
174
+ const humidityPercent = snapshot.humidityPercent;
175
+ if (humidityPercent !== undefined) {
176
+ const humiditySensorChild = airConditioner.getChildEndpointById('HumiditySensor');
177
+ if (humiditySensorChild) {
178
+ await humiditySensorChild.updateAttribute(RelativeHumidityMeasurement.id, 'measuredValue', humidityPercent * 100, logger);
179
+ }
180
+ }
181
+ }
182
+ if (capabilities.supportsAirQualitySensor) {
183
+ const airQualitySensorChild = airConditioner.getChildEndpointById('AirQualitySensor');
184
+ if (airQualitySensorChild) {
185
+ const airQualityOverall = snapshot.airQualityOverall;
186
+ if (airQualityOverall !== undefined) {
187
+ await airQualitySensorChild.updateAttribute(AirQuality.id, 'airQuality', airQualityOverall, logger);
188
+ }
189
+ const pm25Value = snapshot.pm25;
190
+ if (pm25Value !== undefined) {
191
+ await airQualitySensorChild.updateAttribute(Pm25ConcentrationMeasurement.id, 'measuredValue', pm25Value, logger);
192
+ }
193
+ const pm10Value = snapshot.pm10;
194
+ if (pm10Value !== undefined) {
195
+ await airQualitySensorChild.updateAttribute(Pm10ConcentrationMeasurement.id, 'measuredValue', pm10Value, logger);
196
+ }
197
+ }
198
+ }
199
+ if (capabilities.supportsEnergyMonitoring) {
200
+ const watts = isKnownOff(airConditioner, snapshot) ? 0 : snapshot.powerConsumptionWatts;
201
+ if (watts !== undefined) {
202
+ await airConditioner.updateAttribute(ElectricalPowerMeasurement.id, 'activePower', Math.round(watts * 1000), logger);
203
+ }
204
+ }
205
+ }
@@ -0,0 +1,26 @@
1
+ import { isAirConditionerDevice, isWasherDevice } from '../../core/domain/entities/ThinqDevice.js';
2
+ export function reconcileThinqDeviceConfigEntries(configDevices, discoveredDevices) {
3
+ let changed = false;
4
+ for (const discovered of discoveredDevices) {
5
+ let deviceType;
6
+ if (isAirConditionerDevice(discovered)) {
7
+ deviceType = 'AC';
8
+ }
9
+ else if (isWasherDevice(discovered)) {
10
+ deviceType = 'WASHER';
11
+ }
12
+ else {
13
+ continue;
14
+ }
15
+ const existingEntry = configDevices.find((d) => d.deviceId === discovered.id);
16
+ if (!existingEntry) {
17
+ configDevices.push({ deviceId: discovered.id, deviceType });
18
+ changed = true;
19
+ }
20
+ else if (existingEntry.deviceType !== deviceType) {
21
+ existingEntry.deviceType = deviceType;
22
+ changed = true;
23
+ }
24
+ }
25
+ return changed;
26
+ }
@@ -0,0 +1,124 @@
1
+ import { FanControl } from 'matterbridge/matter/clusters';
2
+ import { registerAirConditionerCommandHandlers, THINQ_FAN_SPEED_AUTO, THINQ_FAN_SPEED_LOW, THINQ_FAN_SPEED_MEDIUM, } from './thinqAirConditionerCommandHandlers.js';
3
+ import { buildAirConditionerEndpoint } from './thinqAirConditionerEndpointFactory.js';
4
+ import { registerFilterResetCommandHandler } from './thinqAirConditionerFilterResetCommandHandler.js';
5
+ import { registerWasherCommandHandlers } from './thinqWasherCommandHandlers.js';
6
+ import { buildWasherEndpoint } from './thinqWasherEndpointFactory.js';
7
+ import { extractWasherStopCommand } from './thinqWasherStopCommandResolver.js';
8
+ const DEFAULT_TEMPERATURE_CELSIUS = 20;
9
+ const MAX_HEAT_SETPOINT_LIMIT_CELSIUS = 30;
10
+ const MIN_COOL_SETPOINT_LIMIT_CELSIUS = 18;
11
+ const DEFAULT_PRODUCT_IDENTITY = {
12
+ AC: { productId: 0x8000, productName: 'LG Air Conditioner' },
13
+ WASHER: { productId: 0x8001, productName: 'LG Washer' },
14
+ };
15
+ export function mapWindStrengthToFanMode(windStrength) {
16
+ if (windStrength === undefined || windStrength === THINQ_FAN_SPEED_AUTO) {
17
+ return FanControl.FanMode.Auto;
18
+ }
19
+ if (windStrength <= THINQ_FAN_SPEED_LOW) {
20
+ return FanControl.FanMode.Low;
21
+ }
22
+ if (windStrength <= THINQ_FAN_SPEED_MEDIUM) {
23
+ return FanControl.FanMode.Medium;
24
+ }
25
+ return FanControl.FanMode.High;
26
+ }
27
+ export function mapWindStrengthToFixedFanMode(windStrength) {
28
+ return windStrength === undefined ? FanControl.FanMode.Off : FanControl.FanMode.High;
29
+ }
30
+ export class ThinqDeviceConfigurator {
31
+ logger;
32
+ apiClient;
33
+ configManager;
34
+ constructor(logger, apiClient, configManager) {
35
+ this.logger = logger;
36
+ this.apiClient = apiClient;
37
+ this.configManager = configManager;
38
+ }
39
+ resolveProductIdentity(device) {
40
+ const productIdOverride = this.configManager.getProductIdForDevice(device.id);
41
+ const productNameOverride = this.configManager.getProductNameForDevice(device.id);
42
+ const defaultIdentity = DEFAULT_PRODUCT_IDENTITY[device.type];
43
+ return {
44
+ productId: productIdOverride ?? defaultIdentity.productId,
45
+ productName: productNameOverride ?? defaultIdentity.productName,
46
+ };
47
+ }
48
+ async registerAirConditioner(device) {
49
+ const snapshot = device.snapshot;
50
+ const currentTemperature = snapshot.currentTemperatureCelsius ?? DEFAULT_TEMPERATURE_CELSIUS;
51
+ const targetTemperature = snapshot.targetTemperatureCelsius ?? DEFAULT_TEMPERATURE_CELSIUS;
52
+ const capabilities = this.configManager.getDeviceCapabilities(device.id);
53
+ this.logger.debug(`registerAirConditioner: entry for deviceId=${device.id}`);
54
+ this.logger.info(`Registering ThinQ AirConditioner: ${device.name} (${device.id})`);
55
+ const initialFanMode = capabilities.supportsFanSpeedControl
56
+ ? mapWindStrengthToFanMode(snapshot.windStrength)
57
+ : mapWindStrengthToFixedFanMode(snapshot.windStrength);
58
+ const matterOverride = this.configManager.overrideMatterConfiguration
59
+ ? this.configManager.matterOverrideSettings
60
+ : undefined;
61
+ const productIdentity = this.resolveProductIdentity(device);
62
+ const airConditioner = buildAirConditionerEndpoint(device, capabilities, {
63
+ currentTemperature,
64
+ targetTemperature,
65
+ minHeatSetpointLimitCelsius: 0,
66
+ maxHeatSetpointLimitCelsius: MAX_HEAT_SETPOINT_LIMIT_CELSIUS,
67
+ minCoolSetpointLimitCelsius: MIN_COOL_SETPOINT_LIMIT_CELSIUS,
68
+ maxCoolSetpointLimitCelsius: 50,
69
+ }, initialFanMode, {
70
+ vendorId: matterOverride?.matterVendorId,
71
+ vendorName: matterOverride?.matterVendorName,
72
+ productId: productIdentity.productId,
73
+ productName: productIdentity.productName,
74
+ })
75
+ .createDefaultTemperatureMeasurementClusterServer(currentTemperature * 100)
76
+ .addRequiredClusterServers();
77
+ airConditioner.mode = 'server';
78
+ registerAirConditionerCommandHandlers(airConditioner, device, this.apiClient, this.logger, capabilities);
79
+ const filterResetControl = this.configManager.getAcFilterControlConfig(device.id);
80
+ registerFilterResetCommandHandler(airConditioner, device, this.apiClient, this.logger, capabilities, filterResetControl);
81
+ this.logger.debug(`registerAirConditioner: completed for deviceId=${device.id}`);
82
+ return Promise.resolve(airConditioner);
83
+ }
84
+ async registerWasher(device) {
85
+ this.logger.debug(`registerWasher: entry for deviceId=${device.id}`);
86
+ this.logger.info(`Registering ThinQ Washer: ${device.name} (${device.id})`);
87
+ const matterOverride = this.configManager.overrideMatterConfiguration
88
+ ? this.configManager.matterOverrideSettings
89
+ : undefined;
90
+ const productIdentity = this.resolveProductIdentity(device);
91
+ const washer = buildWasherEndpoint(device, {
92
+ vendorId: matterOverride?.matterVendorId,
93
+ vendorName: matterOverride?.matterVendorName,
94
+ productId: productIdentity.productId,
95
+ productName: productIdentity.productName,
96
+ });
97
+ washer.mode = 'server';
98
+ const washerControl = this.configManager.getWasherControlConfig(device.id);
99
+ const stopCommandPayload = await this.resolveWasherStopCommandPayload(device);
100
+ registerWasherCommandHandlers(washer, device, this.apiClient, this.logger, washerControl, stopCommandPayload);
101
+ this.logger.debug(`registerWasher: completed for deviceId=${device.id}`);
102
+ return washer;
103
+ }
104
+ async resolveWasherStopCommandPayload(device) {
105
+ if (!device.modelJsonUri) {
106
+ return undefined;
107
+ }
108
+ try {
109
+ const model = await this.apiClient.getDeviceModel(device.modelJsonUri);
110
+ const payload = extractWasherStopCommand(model);
111
+ if (payload) {
112
+ this.logger.info(`ThinQ Washer ${device.id}: resolved stop-command payload from device model: ${JSON.stringify(payload)}`);
113
+ }
114
+ else {
115
+ this.logger.info(`ThinQ Washer ${device.id}: device model has no WMStop/WMOff entry.`);
116
+ }
117
+ return payload;
118
+ }
119
+ catch (error) {
120
+ this.logger.debug(`ThinQ Washer ${device.id}: failed to resolve stop-command payload from device model: ${error instanceof Error ? error.message : String(error)}`);
121
+ return undefined;
122
+ }
123
+ }
124
+ }