meross-iot 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 (99) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +153 -0
  4. package/index.d.ts +2344 -0
  5. package/index.js +131 -0
  6. package/lib/controller/device.js +1317 -0
  7. package/lib/controller/features/alarm-feature.js +89 -0
  8. package/lib/controller/features/child-lock-feature.js +61 -0
  9. package/lib/controller/features/config-feature.js +54 -0
  10. package/lib/controller/features/consumption-feature.js +210 -0
  11. package/lib/controller/features/control-feature.js +62 -0
  12. package/lib/controller/features/diffuser-feature.js +411 -0
  13. package/lib/controller/features/digest-timer-feature.js +22 -0
  14. package/lib/controller/features/digest-trigger-feature.js +22 -0
  15. package/lib/controller/features/dnd-feature.js +79 -0
  16. package/lib/controller/features/electricity-feature.js +144 -0
  17. package/lib/controller/features/encryption-feature.js +259 -0
  18. package/lib/controller/features/garage-feature.js +337 -0
  19. package/lib/controller/features/hub-feature.js +687 -0
  20. package/lib/controller/features/light-feature.js +408 -0
  21. package/lib/controller/features/presence-sensor-feature.js +297 -0
  22. package/lib/controller/features/roller-shutter-feature.js +456 -0
  23. package/lib/controller/features/runtime-feature.js +74 -0
  24. package/lib/controller/features/screen-feature.js +67 -0
  25. package/lib/controller/features/sensor-history-feature.js +47 -0
  26. package/lib/controller/features/smoke-config-feature.js +50 -0
  27. package/lib/controller/features/spray-feature.js +166 -0
  28. package/lib/controller/features/system-feature.js +269 -0
  29. package/lib/controller/features/temp-unit-feature.js +55 -0
  30. package/lib/controller/features/thermostat-feature.js +804 -0
  31. package/lib/controller/features/timer-feature.js +507 -0
  32. package/lib/controller/features/toggle-feature.js +223 -0
  33. package/lib/controller/features/trigger-feature.js +333 -0
  34. package/lib/controller/hub-device.js +185 -0
  35. package/lib/controller/subdevice.js +1537 -0
  36. package/lib/device-factory.js +463 -0
  37. package/lib/error-budget.js +138 -0
  38. package/lib/http-api.js +766 -0
  39. package/lib/manager.js +1609 -0
  40. package/lib/model/channel-info.js +79 -0
  41. package/lib/model/constants.js +119 -0
  42. package/lib/model/enums.js +819 -0
  43. package/lib/model/exception.js +363 -0
  44. package/lib/model/http/device.js +215 -0
  45. package/lib/model/http/error-codes.js +121 -0
  46. package/lib/model/http/exception.js +151 -0
  47. package/lib/model/http/subdevice.js +133 -0
  48. package/lib/model/push/alarm.js +112 -0
  49. package/lib/model/push/bind.js +97 -0
  50. package/lib/model/push/common.js +282 -0
  51. package/lib/model/push/diffuser-light.js +100 -0
  52. package/lib/model/push/diffuser-spray.js +83 -0
  53. package/lib/model/push/factory.js +229 -0
  54. package/lib/model/push/generic.js +115 -0
  55. package/lib/model/push/hub-battery.js +59 -0
  56. package/lib/model/push/hub-mts100-all.js +64 -0
  57. package/lib/model/push/hub-mts100-mode.js +59 -0
  58. package/lib/model/push/hub-mts100-temperature.js +62 -0
  59. package/lib/model/push/hub-online.js +59 -0
  60. package/lib/model/push/hub-sensor-alert.js +61 -0
  61. package/lib/model/push/hub-sensor-all.js +59 -0
  62. package/lib/model/push/hub-sensor-smoke.js +110 -0
  63. package/lib/model/push/hub-sensor-temphum.js +62 -0
  64. package/lib/model/push/hub-subdevicelist.js +50 -0
  65. package/lib/model/push/hub-togglex.js +60 -0
  66. package/lib/model/push/index.js +81 -0
  67. package/lib/model/push/online.js +53 -0
  68. package/lib/model/push/presence-study.js +61 -0
  69. package/lib/model/push/sensor-latestx.js +106 -0
  70. package/lib/model/push/timerx.js +63 -0
  71. package/lib/model/push/togglex.js +78 -0
  72. package/lib/model/push/triggerx.js +62 -0
  73. package/lib/model/push/unbind.js +34 -0
  74. package/lib/model/push/water-leak.js +107 -0
  75. package/lib/model/states/diffuser-light-state.js +119 -0
  76. package/lib/model/states/diffuser-spray-state.js +58 -0
  77. package/lib/model/states/garage-door-state.js +71 -0
  78. package/lib/model/states/index.js +38 -0
  79. package/lib/model/states/light-state.js +134 -0
  80. package/lib/model/states/presence-sensor-state.js +239 -0
  81. package/lib/model/states/roller-shutter-state.js +82 -0
  82. package/lib/model/states/spray-state.js +58 -0
  83. package/lib/model/states/thermostat-state.js +297 -0
  84. package/lib/model/states/timer-state.js +192 -0
  85. package/lib/model/states/toggle-state.js +105 -0
  86. package/lib/model/states/trigger-state.js +155 -0
  87. package/lib/subscription.js +587 -0
  88. package/lib/utilities/conversion.js +62 -0
  89. package/lib/utilities/debug.js +165 -0
  90. package/lib/utilities/mqtt.js +152 -0
  91. package/lib/utilities/network.js +53 -0
  92. package/lib/utilities/options.js +64 -0
  93. package/lib/utilities/request-queue.js +161 -0
  94. package/lib/utilities/ssid.js +37 -0
  95. package/lib/utilities/state-changes.js +66 -0
  96. package/lib/utilities/stats.js +687 -0
  97. package/lib/utilities/timer.js +310 -0
  98. package/lib/utilities/trigger.js +286 -0
  99. package/package.json +73 -0
@@ -0,0 +1,804 @@
1
+ 'use strict';
2
+
3
+ const ThermostatState = require('../../model/states/thermostat-state');
4
+ const { normalizeChannel } = require('../../utilities/options');
5
+ const { buildStateChanges } = require('../../utilities/state-changes');
6
+
7
+ /**
8
+ * Thermostat feature module.
9
+ * Provides control over thermostat mode, temperature settings, schedules, and various configuration options.
10
+ */
11
+ module.exports = {
12
+ /**
13
+ * Controls the thermostat mode.
14
+ *
15
+ * Supports both ThermostatMode enum objects and numeric values. Temperature values are automatically
16
+ * converted from Celsius to device units and rounded to the nearest 0.5°C increment.
17
+ *
18
+ * @param {Object} options - Thermostat mode options
19
+ * @param {number} [options.channel=0] - Channel to control (default: 0)
20
+ * @param {number|import('../lib/enums').ThermostatMode} [options.mode] - Thermostat mode (from ThermostatMode enum or numeric value)
21
+ * @param {number} [options.onoff] - On/off state (0=off, 1=on)
22
+ * @param {number} [options.heatTemperature] - Heat temperature in Celsius (will be converted to device units)
23
+ * @param {number} [options.coolTemperature] - Cool temperature in Celsius (will be converted to device units)
24
+ * @param {number} [options.ecoTemperature] - Eco temperature in Celsius (will be converted to device units)
25
+ * @param {number} [options.manualTemperature] - Manual temperature in Celsius (will be converted to device units)
26
+ * @param {boolean} [options.partialUpdate=false] - If true, fetches current state and merges with provided options
27
+ * @returns {Promise<Object>} Response from the device
28
+ * @throws {import('../lib/errors/errors').CommandError} If mode value is invalid or temperature is out of range
29
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
30
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
31
+ */
32
+ async setThermostatMode(options = {}) {
33
+ const channel = normalizeChannel(options);
34
+ let processedModeData = { ...options };
35
+
36
+ if (options.partialUpdate) {
37
+ try {
38
+ const currentResponse = await this.getThermostatMode({ channel });
39
+ if (currentResponse && currentResponse.mode && Array.isArray(currentResponse.mode) && currentResponse.mode.length > 0) {
40
+ const currentState = currentResponse.mode[0];
41
+ processedModeData = { ...currentState, ...processedModeData };
42
+ }
43
+ } catch (e) {
44
+ // If fetch fails, continue with provided options only
45
+ }
46
+ }
47
+
48
+ processedModeData.channel = channel;
49
+
50
+ // Mode must be a number
51
+ if (processedModeData.mode !== undefined && processedModeData.mode !== null && typeof processedModeData.mode !== 'number') {
52
+ processedModeData.mode = 0;
53
+ }
54
+
55
+ if (processedModeData.heatTemperature !== undefined) {
56
+ processedModeData.heatTemp = this._alignThermostatTemperature(processedModeData.heatTemperature, channel);
57
+ delete processedModeData.heatTemperature;
58
+ }
59
+ if (processedModeData.coolTemperature !== undefined) {
60
+ processedModeData.coolTemp = this._alignThermostatTemperature(processedModeData.coolTemperature, channel);
61
+ delete processedModeData.coolTemperature;
62
+ }
63
+ if (processedModeData.ecoTemperature !== undefined) {
64
+ processedModeData.ecoTemp = this._alignThermostatTemperature(processedModeData.ecoTemperature, channel);
65
+ delete processedModeData.ecoTemperature;
66
+ }
67
+ if (processedModeData.manualTemperature !== undefined) {
68
+ processedModeData.manualTemp = this._alignThermostatTemperature(processedModeData.manualTemperature, channel);
69
+ delete processedModeData.manualTemperature;
70
+ }
71
+
72
+ delete processedModeData.partialUpdate;
73
+
74
+ const payload = { 'mode': [processedModeData] };
75
+ const response = await this.publishMessage('SET', 'Appliance.Control.Thermostat.Mode', payload);
76
+ if (response && response.mode) {
77
+ this._updateThermostatMode(response.mode, 'response');
78
+ this._lastFullUpdateTimestamp = Date.now();
79
+ }
80
+ return response;
81
+ },
82
+
83
+ /**
84
+ * Controls the thermostat mode B.
85
+ *
86
+ * Mode B is an alternative mode system used by some thermostat models. Supports both ThermostatModeBState
87
+ * enum objects and numeric values. Throws an error if the device does not support the ModeB namespace.
88
+ *
89
+ * @param {Object} options - Thermostat mode B options
90
+ * @param {number} [options.channel=0] - Channel to control (default: 0)
91
+ * @param {number|import('../lib/enums').ThermostatModeBState} [options.state] - Mode B state (from ThermostatModeBState enum or numeric value)
92
+ * @returns {Promise<Object>} Response from the device
93
+ * @throws {import('../lib/errors/errors').CommandError} If the device does not support the ModeB namespace or state value is invalid
94
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
95
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
96
+ */
97
+ async setThermostatModeB(options = {}) {
98
+ const channel = normalizeChannel(options);
99
+ if (!this._abilities || !this._abilities['Appliance.Control.Thermostat.ModeB']) {
100
+ const { CommandError } = require('../../model/exception');
101
+ throw new CommandError(
102
+ 'Device does not support Appliance.Control.Thermostat.ModeB namespace',
103
+ { namespace: 'Appliance.Control.Thermostat.ModeB', channel, options },
104
+ this.uuid
105
+ );
106
+ }
107
+
108
+ const processedModeData = { ...options };
109
+ processedModeData.channel = channel;
110
+
111
+ // State must be a number
112
+ if (processedModeData.state !== undefined && processedModeData.state !== null && typeof processedModeData.state !== 'number') {
113
+ processedModeData.state = 1;
114
+ }
115
+
116
+ const payload = { 'modeB': [processedModeData] };
117
+ const response = await this.publishMessage('SET', 'Appliance.Control.Thermostat.ModeB', payload);
118
+ if (response && response.modeB) {
119
+ this._updateThermostatModeB(response.modeB, 'response');
120
+ }
121
+ return response;
122
+ },
123
+
124
+ /**
125
+ * Controls the thermostat window opened status.
126
+ *
127
+ * Used to inform the thermostat when a window is opened or closed, which may affect heating/cooling behavior.
128
+ *
129
+ * @param {Object} options - Window opened options
130
+ * @param {number} [options.channel=0] - Channel to control (default: 0)
131
+ * @param {boolean} options.windowOpened - True if window is opened, false if closed
132
+ * @returns {Promise<Object>} Response from the device
133
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
134
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
135
+ */
136
+ async setThermostatWindowOpened(options = {}) {
137
+ const channel = normalizeChannel(options);
138
+ if (options.windowOpened === undefined) {
139
+ throw new Error('windowOpened is required');
140
+ }
141
+ const payload = { 'windowOpened': [{ channel, 'status': options.windowOpened ? 1 : 0 }] };
142
+ return await this.publishMessage('SET', 'Appliance.Control.Thermostat.WindowOpened', payload);
143
+ },
144
+
145
+ /**
146
+ * Gets the current thermostat mode from the device.
147
+ *
148
+ * Use {@link getCachedThermostatState} to get cached state without making a request.
149
+ *
150
+ * @param {Object} [options={}] - Get options
151
+ * @param {number} [options.channel=0] - Channel to get mode for (default: 0)
152
+ * @returns {Promise<Object>} Response containing thermostat mode
153
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
154
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
155
+ */
156
+ async getThermostatMode(options = {}) {
157
+ const channel = normalizeChannel(options);
158
+ const payload = { 'mode': [{ channel }] };
159
+ const response = await this.publishMessage('GET', 'Appliance.Control.Thermostat.Mode', payload);
160
+ if (response && response.mode) {
161
+ this._updateThermostatMode(response.mode, 'response');
162
+ this._lastFullUpdateTimestamp = Date.now();
163
+ }
164
+ return response;
165
+ },
166
+
167
+ /**
168
+ * Gets the current thermostat mode B from the device.
169
+ *
170
+ * Throws an error if the device does not support the ModeB namespace. Use {@link getCachedThermostatModeBState}
171
+ * to get cached state without making a request.
172
+ *
173
+ * @param {Object} [options={}] - Get options
174
+ * @param {number} [options.channel=0] - Channel to get mode B for (default: 0)
175
+ * @returns {Promise<Object>} Response containing thermostat mode B
176
+ * @throws {import('../lib/errors/errors').CommandError} If the device does not support the ModeB namespace
177
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
178
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
179
+ */
180
+ async getThermostatModeB(options = {}) {
181
+ const channel = normalizeChannel(options);
182
+ if (!this._abilities || !this._abilities['Appliance.Control.Thermostat.ModeB']) {
183
+ const { CommandError } = require('../../model/exception');
184
+ throw new CommandError(
185
+ 'Device does not support Appliance.Control.Thermostat.ModeB namespace',
186
+ { namespace: 'Appliance.Control.Thermostat.ModeB', channel },
187
+ this.uuid
188
+ );
189
+ }
190
+
191
+ const payload = { 'modeB': [{ channel }] };
192
+ const response = await this.publishMessage('GET', 'Appliance.Control.Thermostat.ModeB', payload);
193
+ if (response && response.modeB) {
194
+ this._updateThermostatModeB(response.modeB, 'response');
195
+ this._lastFullUpdateTimestamp = Date.now();
196
+ }
197
+ return response;
198
+ },
199
+
200
+ /**
201
+ * Gets the thermostat window opened status from the device.
202
+ *
203
+ * @param {Object} [options={}] - Get options
204
+ * @param {number} [options.channel=0] - Channel to get window opened status for (default: 0)
205
+ * @returns {Promise<Object>} Response containing window opened status
206
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
207
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
208
+ */
209
+ async getThermostatWindowOpened(options = {}) {
210
+ const channel = normalizeChannel(options);
211
+ const payload = { 'windowOpened': [{ channel }] };
212
+ return await this.publishMessage('GET', 'Appliance.Control.Thermostat.WindowOpened', payload);
213
+ },
214
+
215
+ /**
216
+ * Gets the thermostat schedule from the device.
217
+ * @param {Object} [options={}] - Get options
218
+ * @param {number} [options.channel=0] - Channel to get schedule for (default: 0)
219
+ * @returns {Promise<Object>} Response containing thermostat schedule
220
+ */
221
+ async getThermostatSchedule(options = {}) {
222
+ const channel = normalizeChannel(options);
223
+ const payload = {
224
+ schedule: [{
225
+ channel
226
+ }]
227
+ };
228
+ return await this.publishMessage('GET', 'Appliance.Control.Thermostat.Schedule', payload);
229
+ },
230
+
231
+ /**
232
+ * Controls (sets) the thermostat schedule.
233
+ * @param {Object} scheduleData - Schedule data object (array of schedule items)
234
+ * @param {number|null} timeout - Optional timeout in milliseconds
235
+ * @returns {Promise<Object>} Response from the device
236
+ */
237
+ async setThermostatSchedule(scheduleData) {
238
+ const payload = { schedule: Array.isArray(scheduleData) ? scheduleData : [scheduleData] };
239
+ return await this.publishMessage('SET', 'Appliance.Control.Thermostat.Schedule', payload);
240
+ },
241
+
242
+ /**
243
+ * Gets the thermostat timer configuration from the device.
244
+ * @param {Object} [options={}] - Get options
245
+ * @param {number} [options.channel=0] - Channel to get timer for (default: 0)
246
+ * @returns {Promise<Object>} Response containing thermostat timer data
247
+ */
248
+ async getThermostatTimer(options = {}) {
249
+ const channel = normalizeChannel(options);
250
+ const payload = {
251
+ timer: [{
252
+ channel
253
+ }]
254
+ };
255
+ return await this.publishMessage('GET', 'Appliance.Control.Thermostat.Timer', payload);
256
+ },
257
+
258
+ /**
259
+ * Controls (sets) the thermostat timer configuration.
260
+ * @param {Object} timerData - Timer data object (array of timer items)
261
+ * @param {number|null} timeout - Optional timeout in milliseconds
262
+ * @returns {Promise<Object>} Response from the device
263
+ */
264
+ async setThermostatTimer(timerData) {
265
+ const payload = { timer: Array.isArray(timerData) ? timerData : [timerData] };
266
+ return await this.publishMessage('SET', 'Appliance.Control.Thermostat.Timer', payload);
267
+ },
268
+
269
+ /**
270
+ * Acknowledges a thermostat alarm (device-initiated SET).
271
+ *
272
+ * Alarm events are typically initiated by the device via SET. This method sends a SETACK response
273
+ * to acknowledge receipt of the alarm event.
274
+ *
275
+ * @param {Object} [options={}] - Acknowledge options
276
+ * @param {number} [options.channel=0] - Channel to acknowledge alarm for (default: 0)
277
+ * @returns {Promise<Object>} Response from the device
278
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
279
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
280
+ */
281
+ async acknowledgeThermostatAlarm(options = {}) {
282
+ const channel = normalizeChannel(options);
283
+ const payload = {
284
+ alarm: [{
285
+ channel,
286
+ temp: 0,
287
+ type: 0
288
+ }]
289
+ };
290
+ return await this.publishMessage('SETACK', 'Appliance.Control.Thermostat.Alarm', payload);
291
+ },
292
+
293
+ /**
294
+ * Gets the thermostat hold action configuration from the device.
295
+ * @param {Object} [options={}] - Get options
296
+ * @param {number} [options.channel=0] - Channel to get hold action for (default: 0)
297
+ * @returns {Promise<Object>} Response containing hold action configuration
298
+ */
299
+ async getThermostatHoldAction(options = {}) {
300
+ const channel = normalizeChannel(options);
301
+ const payload = {
302
+ holdAction: [{
303
+ channel
304
+ }]
305
+ };
306
+ return await this.publishMessage('GET', 'Appliance.Control.Thermostat.HoldAction', payload);
307
+ },
308
+
309
+ /**
310
+ * Controls (sets) the thermostat hold action configuration.
311
+ * @param {Object} options - Hold action options
312
+ * @param {Object|Array} options.holdActionData - Hold action data object (array of hold action items)
313
+ * @returns {Promise<Object>} Response from the device
314
+ */
315
+ async setThermostatHoldAction(options = {}) {
316
+ if (!options.holdActionData) {
317
+ throw new Error('holdActionData is required');
318
+ }
319
+ const payload = { holdAction: Array.isArray(options.holdActionData) ? options.holdActionData : [options.holdActionData] };
320
+ return await this.publishMessage('SET', 'Appliance.Control.Thermostat.HoldAction', payload);
321
+ },
322
+
323
+ /**
324
+ * Gets the thermostat hold action configuration from the device.
325
+ * @param {Object} [options={}] - Get options
326
+ * @param {number} [options.channel=0] - Channel to get hold action for (default: 0)
327
+ * @returns {Promise<Object>} Response containing hold action configuration
328
+ */
329
+ async getThermostatHoldAction(options = {}) {
330
+ const channel = normalizeChannel(options);
331
+ const payload = {
332
+ holdAction: [{
333
+ channel
334
+ }]
335
+ };
336
+ return await this.publishMessage('GET', 'Appliance.Control.Thermostat.HoldAction', payload);
337
+ },
338
+
339
+ /**
340
+ * Controls (sets) the thermostat hold action configuration.
341
+ * @param {Object} options - Hold action options
342
+ * @param {Object|Array} options.holdActionData - Hold action data object (array of hold action items)
343
+ * @returns {Promise<Object>} Response from the device
344
+ */
345
+ async setThermostatHoldAction(options = {}) {
346
+ if (!options.holdActionData) {
347
+ throw new Error('holdActionData is required');
348
+ }
349
+ const payload = { holdAction: Array.isArray(options.holdActionData) ? options.holdActionData : [options.holdActionData] };
350
+ return await this.publishMessage('SET', 'Appliance.Control.Thermostat.HoldAction', payload);
351
+ },
352
+
353
+ /**
354
+ * Gets the thermostat overheat protection configuration from the device.
355
+ * @param {Object} [options={}] - Get options
356
+ * @param {number} [options.channel=0] - Channel to get overheat config for (default: 0)
357
+ * @returns {Promise<Object>} Response containing overheat configuration
358
+ */
359
+ async getThermostatOverheat(options = {}) {
360
+ const channel = normalizeChannel(options);
361
+ const payload = {
362
+ overheat: [{
363
+ channel
364
+ }]
365
+ };
366
+ return await this.publishMessage('GET', 'Appliance.Control.Thermostat.Overheat', payload);
367
+ },
368
+
369
+ /**
370
+ * Controls (sets) the thermostat overheat protection configuration.
371
+ * @param {Object} options - Overheat options
372
+ * @param {Object|Array} options.overheatData - Overheat data object (array of overheat items)
373
+ * @returns {Promise<Object>} Response from the device
374
+ */
375
+ async setThermostatOverheat(options = {}) {
376
+ if (!options.overheatData) {
377
+ throw new Error('overheatData is required');
378
+ }
379
+ const payload = { overheat: Array.isArray(options.overheatData) ? options.overheatData : [options.overheatData] };
380
+ return await this.publishMessage('SET', 'Appliance.Control.Thermostat.Overheat', payload);
381
+ },
382
+
383
+ /**
384
+ * Gets the thermostat dead zone configuration from the device.
385
+ * @param {Object} [options={}] - Get options
386
+ * @param {number} [options.channel=0] - Channel to get dead zone for (default: 0)
387
+ * @returns {Promise<Object>} Response containing dead zone configuration
388
+ */
389
+ async getThermostatDeadZone(options = {}) {
390
+ const channel = normalizeChannel(options);
391
+ const payload = {
392
+ deadZone: [{
393
+ channel
394
+ }]
395
+ };
396
+ return await this.publishMessage('GET', 'Appliance.Control.Thermostat.DeadZone', payload);
397
+ },
398
+
399
+ /**
400
+ * Controls (sets) the thermostat dead zone configuration.
401
+ * @param {Object} options - Dead zone options
402
+ * @param {Object|Array} options.deadZoneData - Dead zone data object (array of dead zone items)
403
+ * @returns {Promise<Object>} Response from the device
404
+ */
405
+ async setThermostatDeadZone(options = {}) {
406
+ if (!options.deadZoneData) {
407
+ throw new Error('deadZoneData is required');
408
+ }
409
+ const payload = { deadZone: Array.isArray(options.deadZoneData) ? options.deadZoneData : [options.deadZoneData] };
410
+ return await this.publishMessage('SET', 'Appliance.Control.Thermostat.DeadZone', payload);
411
+ },
412
+
413
+ /**
414
+ * Gets the thermostat calibration configuration from the device.
415
+ * @param {Object} [options={}] - Get options
416
+ * @param {number} [options.channel=0] - Channel to get calibration for (default: 0)
417
+ * @returns {Promise<Object>} Response containing calibration configuration
418
+ */
419
+ async getThermostatCalibration(options = {}) {
420
+ const channel = normalizeChannel(options);
421
+ const payload = {
422
+ calibration: [{
423
+ channel
424
+ }]
425
+ };
426
+ return await this.publishMessage('GET', 'Appliance.Control.Thermostat.Calibration', payload);
427
+ },
428
+
429
+ /**
430
+ * Controls (sets) the thermostat calibration configuration.
431
+ * @param {Object} options - Calibration options
432
+ * @param {Object|Array} options.calibrationData - Calibration data object (array of calibration items)
433
+ * @returns {Promise<Object>} Response from the device
434
+ */
435
+ async setThermostatCalibration(options = {}) {
436
+ if (!options.calibrationData) {
437
+ throw new Error('calibrationData is required');
438
+ }
439
+ const payload = { calibration: Array.isArray(options.calibrationData) ? options.calibrationData : [options.calibrationData] };
440
+ return await this.publishMessage('SET', 'Appliance.Control.Thermostat.Calibration', payload);
441
+ },
442
+
443
+ /**
444
+ * Gets the thermostat sensor mode configuration from the device.
445
+ * @param {Object} [options={}] - Get options
446
+ * @param {number} [options.channel=0] - Channel to get sensor mode for (default: 0)
447
+ * @returns {Promise<Object>} Response containing sensor mode configuration
448
+ */
449
+ async getThermostatSensor(options = {}) {
450
+ const channel = normalizeChannel(options);
451
+ const payload = {
452
+ sensor: [{
453
+ channel
454
+ }]
455
+ };
456
+ return await this.publishMessage('GET', 'Appliance.Control.Thermostat.Sensor', payload);
457
+ },
458
+
459
+ /**
460
+ * Controls (sets) the thermostat sensor mode configuration.
461
+ * @param {Object} options - Sensor options
462
+ * @param {Object|Array} options.sensorData - Sensor data object (array of sensor items)
463
+ * @returns {Promise<Object>} Response from the device
464
+ */
465
+ async setThermostatSensor(options = {}) {
466
+ if (!options.sensorData) {
467
+ throw new Error('sensorData is required');
468
+ }
469
+ const payload = { sensor: Array.isArray(options.sensorData) ? options.sensorData : [options.sensorData] };
470
+ return await this.publishMessage('SET', 'Appliance.Control.Thermostat.Sensor', payload);
471
+ },
472
+
473
+ /**
474
+ * Gets the thermostat summer mode configuration from the device.
475
+ * @param {Object} [options={}] - Get options
476
+ * @param {number} [options.channel=0] - Channel to get summer mode for (default: 0)
477
+ * @returns {Promise<Object>} Response containing summer mode configuration
478
+ */
479
+ async getThermostatSummerMode(options = {}) {
480
+ const channel = normalizeChannel(options);
481
+ const payload = {
482
+ summerMode: [{
483
+ channel
484
+ }]
485
+ };
486
+ return await this.publishMessage('GET', 'Appliance.Control.Thermostat.SummerMode', payload);
487
+ },
488
+
489
+ /**
490
+ * Controls (sets) the thermostat summer mode configuration.
491
+ * @param {Object} options - Summer mode options
492
+ * @param {Object|Array} options.summerModeData - Summer mode data object (array of summer mode items)
493
+ * @returns {Promise<Object>} Response from the device
494
+ */
495
+ async setThermostatSummerMode(options = {}) {
496
+ if (!options.summerModeData) {
497
+ throw new Error('summerModeData is required');
498
+ }
499
+ const payload = { summerMode: Array.isArray(options.summerModeData) ? options.summerModeData : [options.summerModeData] };
500
+ return await this.publishMessage('SET', 'Appliance.Control.Thermostat.SummerMode', payload);
501
+ },
502
+
503
+ /**
504
+ * Gets the thermostat frost protection configuration from the device.
505
+ * @param {Object} [options={}] - Get options
506
+ * @param {number} [options.channel=0] - Channel to get frost config for (default: 0)
507
+ * @returns {Promise<Object>} Response containing frost configuration
508
+ */
509
+ async getThermostatFrost(options = {}) {
510
+ const channel = normalizeChannel(options);
511
+ const payload = {
512
+ frost: [{
513
+ channel
514
+ }]
515
+ };
516
+ return await this.publishMessage('GET', 'Appliance.Control.Thermostat.Frost', payload);
517
+ },
518
+
519
+ /**
520
+ * Controls (sets) the thermostat frost protection configuration.
521
+ * @param {Object} options - Frost options
522
+ * @param {Object|Array} options.frostData - Frost data object (array of frost items)
523
+ * @returns {Promise<Object>} Response from the device
524
+ */
525
+ async setThermostatFrost(options = {}) {
526
+ if (!options.frostData) {
527
+ throw new Error('frostData is required');
528
+ }
529
+ const payload = { frost: Array.isArray(options.frostData) ? options.frostData : [options.frostData] };
530
+ return await this.publishMessage('SET', 'Appliance.Control.Thermostat.Frost', payload);
531
+ },
532
+
533
+ /**
534
+ * Gets the thermostat alarm configuration from the device.
535
+ * @param {Object} [options={}] - Get options
536
+ * @param {number} [options.channel=0] - Channel to get alarm config for (default: 0)
537
+ * @returns {Promise<Object>} Response containing alarm configuration
538
+ */
539
+ async getThermostatAlarmConfig(options = {}) {
540
+ const channel = normalizeChannel(options);
541
+ const payload = {
542
+ alarmConfig: [{
543
+ channel
544
+ }]
545
+ };
546
+ return await this.publishMessage('GET', 'Appliance.Control.Thermostat.AlarmConfig', payload);
547
+ },
548
+
549
+ /**
550
+ * Controls (sets) the thermostat alarm configuration.
551
+ * @param {Object} options - Alarm config options
552
+ * @param {Object|Array} options.alarmConfigData - Alarm config data object (array of alarm config items)
553
+ * @returns {Promise<Object>} Response from the device
554
+ */
555
+ async setThermostatAlarmConfig(options = {}) {
556
+ if (!options.alarmConfigData) {
557
+ throw new Error('alarmConfigData is required');
558
+ }
559
+ const payload = { alarmConfig: Array.isArray(options.alarmConfigData) ? options.alarmConfigData : [options.alarmConfigData] };
560
+ return await this.publishMessage('SET', 'Appliance.Control.Thermostat.AlarmConfig', payload);
561
+ },
562
+
563
+ /**
564
+ * Gets the thermostat compressor delay configuration from the device.
565
+ * @param {Object} [options={}] - Get options
566
+ * @param {number} [options.channel=0] - Channel to get compressor delay for (default: 0)
567
+ * @returns {Promise<Object>} Response containing compressor delay configuration
568
+ */
569
+ async getThermostatCompressorDelay(options = {}) {
570
+ const channel = normalizeChannel(options);
571
+ const payload = {
572
+ delay: [{
573
+ channel
574
+ }]
575
+ };
576
+ return await this.publishMessage('GET', 'Appliance.Control.Thermostat.CompressorDelay', payload);
577
+ },
578
+
579
+ /**
580
+ * Controls (sets) the thermostat compressor delay configuration.
581
+ * @param {Object} options - Delay options
582
+ * @param {Object|Array} options.delayData - Delay data object (array of delay items)
583
+ * @returns {Promise<Object>} Response from the device
584
+ */
585
+ async setThermostatCompressorDelay(options = {}) {
586
+ if (!options.delayData) {
587
+ throw new Error('delayData is required');
588
+ }
589
+ const payload = { delay: Array.isArray(options.delayData) ? options.delayData : [options.delayData] };
590
+ return await this.publishMessage('SET', 'Appliance.Control.Thermostat.CompressorDelay', payload);
591
+ },
592
+
593
+ /**
594
+ * Gets the thermostat control range configuration from the device.
595
+ * @param {Object} [options={}] - Get options
596
+ * @param {number} [options.channel=0] - Channel to get control range for (default: 0)
597
+ * @returns {Promise<Object>} Response containing control range configuration
598
+ */
599
+ async getThermostatCtlRange(options = {}) {
600
+ const channel = normalizeChannel(options);
601
+ const payload = {
602
+ ctlRange: [{
603
+ channel
604
+ }]
605
+ };
606
+ return await this.publishMessage('GET', 'Appliance.Control.Thermostat.CtlRange', payload);
607
+ },
608
+
609
+ /**
610
+ * Controls (sets) the thermostat control range configuration.
611
+ * @param {Object} options - Control range options
612
+ * @param {Object|Array} options.ctlRangeData - Control range data object (array of ctlRange items)
613
+ * @returns {Promise<Object>} Response from the device
614
+ */
615
+ async setThermostatCtlRange(options = {}) {
616
+ if (!options.ctlRangeData) {
617
+ throw new Error('ctlRangeData is required');
618
+ }
619
+ const payload = { ctlRange: Array.isArray(options.ctlRangeData) ? options.ctlRangeData : [options.ctlRangeData] };
620
+ return await this.publishMessage('SET', 'Appliance.Control.Thermostat.CtlRange', payload);
621
+ },
622
+
623
+ /**
624
+ * Gets the cached thermostat state for the specified channel.
625
+ *
626
+ * Returns cached state without making a request. Use {@link getThermostatMode} to fetch fresh state
627
+ * from the device. The state object contains enum properties: mode (ThermostatMode), workingMode (ThermostatWorkingMode).
628
+ *
629
+ * @param {number} [channel=0] - Channel to get state for (default: 0)
630
+ * @returns {import('../lib/model/states/thermostat-state').ThermostatState|undefined} Cached thermostat state or undefined if not available
631
+ * @throws {Error} If state has not been initialized (call refreshState() first)
632
+ */
633
+ getCachedThermostatState(channel = 0) {
634
+ this.validateState();
635
+ return this._thermostatStateByChannel.get(channel);
636
+ },
637
+
638
+ /**
639
+ * Gets the cached thermostat mode B state for the specified channel.
640
+ *
641
+ * Returns cached state without making a request. Use {@link getThermostatModeB} to fetch fresh state
642
+ * from the device. The state object contains enum properties: workingMode (ThermostatWorkingMode), state (ThermostatModeBState).
643
+ *
644
+ * @param {number} [channel=0] - Channel to get state for (default: 0)
645
+ * @returns {import('../lib/model/states/thermostat-state').ThermostatState|undefined} Cached thermostat mode B state or undefined if not available
646
+ * @throws {Error} If state has not been initialized (call refreshState() first)
647
+ */
648
+ getCachedThermostatModeBState(channel = 0) {
649
+ this.validateState();
650
+ return this._thermostatStateByChannel.get(channel);
651
+ },
652
+
653
+ /**
654
+ * Aligns temperature value to device units and validates range.
655
+ *
656
+ * Converts Celsius temperature to device units (tenths of degrees) and rounds to nearest 0.5°C increment.
657
+ * Validates against device-specific min/max temperature limits if available in cached state.
658
+ *
659
+ * @param {number} temperature - Temperature in Celsius
660
+ * @param {number} [channel=0] - Channel to get temperature limits for (default: 0)
661
+ * @returns {number} Temperature in device units (tenths of degrees)
662
+ * @throws {import('../lib/errors/errors').CommandError} If temperature is out of valid range
663
+ * @private
664
+ */
665
+ _alignThermostatTemperature(temperature, channel = 0) {
666
+ const THERMOSTAT_MIN_SETTABLE_TEMP = 5.0;
667
+ const THERMOSTAT_MAX_SETTABLE_TEMP = 35.0;
668
+
669
+ let minSetableTemp = THERMOSTAT_MIN_SETTABLE_TEMP;
670
+ let maxSetableTemp = THERMOSTAT_MAX_SETTABLE_TEMP;
671
+
672
+ const channelState = this._thermostatStateByChannel.get(channel);
673
+ if (channelState) {
674
+ if (channelState.minTemperatureCelsius !== undefined) {
675
+ minSetableTemp = channelState.minTemperatureCelsius;
676
+ }
677
+ if (channelState.maxTemperatureCelsius !== undefined) {
678
+ maxSetableTemp = channelState.maxTemperatureCelsius;
679
+ }
680
+ }
681
+
682
+ if (temperature < minSetableTemp || temperature > maxSetableTemp) {
683
+ const { CommandError } = require('../../model/exception');
684
+ throw new CommandError(
685
+ `Temperature ${temperature}°C is out of range (${minSetableTemp}-${maxSetableTemp}°C) for this device`,
686
+ { temperature, minSetableTemp, maxSetableTemp },
687
+ this.uuid
688
+ );
689
+ }
690
+
691
+ const quotient = temperature / 0.5;
692
+ const rounded = Math.round(quotient);
693
+ const finalTemp = rounded * 0.5;
694
+
695
+ return Math.round(finalTemp * 10);
696
+ },
697
+
698
+ /**
699
+ * Updates the cached thermostat mode state from mode data.
700
+ *
701
+ * Called automatically when thermostat mode push notifications are received or commands complete.
702
+ * Handles arrays of mode data for multiple channels.
703
+ *
704
+ * @param {Array} modeData - Array of mode data objects
705
+ * @param {string} [source='response'] - Source of the update ('push' | 'poll' | 'response')
706
+ * @private
707
+ */
708
+ _updateThermostatMode(modeData, source = 'response') {
709
+ if (!modeData || !Array.isArray(modeData)) {return;}
710
+
711
+ for (const channelData of modeData) {
712
+ const channelIndex = channelData.channel;
713
+ if (channelIndex === undefined || channelIndex === null) {continue;}
714
+
715
+ // Get old state before updating
716
+ const oldState = this._thermostatStateByChannel.get(channelIndex);
717
+ const oldValue = oldState ? {
718
+ mode: oldState.mode,
719
+ targetTemp: oldState.targetTemperatureCelsius,
720
+ currentTemp: oldState.currentTemperatureCelsius
721
+ } : undefined;
722
+
723
+ let state = this._thermostatStateByChannel.get(channelIndex);
724
+ if (!state) {
725
+ state = new ThermostatState(channelData);
726
+ this._thermostatStateByChannel.set(channelIndex, state);
727
+ } else {
728
+ state.update(channelData);
729
+ }
730
+
731
+ const newValue = buildStateChanges(oldValue, {
732
+ mode: state.mode,
733
+ targetTemp: state.targetTemperatureCelsius,
734
+ currentTemp: state.currentTemperatureCelsius
735
+ });
736
+
737
+ if (Object.keys(newValue).length > 0) {
738
+ this.emit('stateChange', {
739
+ type: 'thermostat',
740
+ channel: channelIndex,
741
+ value: newValue,
742
+ oldValue,
743
+ source,
744
+ timestamp: Date.now()
745
+ });
746
+ }
747
+ }
748
+ },
749
+
750
+ /**
751
+ * Updates the cached thermostat mode B state from mode B data.
752
+ *
753
+ * Called automatically when thermostat mode B push notifications are received or commands complete.
754
+ * Handles arrays of mode B data for multiple channels.
755
+ *
756
+ * @param {Array} modeData - Array of mode B data objects
757
+ * @param {string} [source='response'] - Source of the update ('push' | 'poll' | 'response')
758
+ * @private
759
+ */
760
+ _updateThermostatModeB(modeData, source = 'response') {
761
+ if (!modeData || !Array.isArray(modeData)) {return;}
762
+
763
+ for (const channelData of modeData) {
764
+ const channelIndex = channelData.channel;
765
+ if (channelIndex === undefined || channelIndex === null) {continue;}
766
+
767
+ // Get old state before updating
768
+ const oldState = this._thermostatStateByChannel.get(channelIndex);
769
+ const oldValue = oldState ? {
770
+ mode: oldState.mode,
771
+ state: oldState.state,
772
+ targetTemp: oldState.targetTemperatureCelsius,
773
+ currentTemp: oldState.currentTemperatureCelsius
774
+ } : undefined;
775
+
776
+ let state = this._thermostatStateByChannel.get(channelIndex);
777
+ if (!state) {
778
+ state = new ThermostatState(channelData);
779
+ this._thermostatStateByChannel.set(channelIndex, state);
780
+ } else {
781
+ state.update(channelData);
782
+ }
783
+
784
+ const newValue = buildStateChanges(oldValue, {
785
+ mode: state.mode,
786
+ state: state.state,
787
+ targetTemp: state.targetTemperatureCelsius,
788
+ currentTemp: state.currentTemperatureCelsius
789
+ });
790
+
791
+ if (Object.keys(newValue).length > 0) {
792
+ this.emit('stateChange', {
793
+ type: 'thermostat',
794
+ channel: channelIndex,
795
+ value: newValue,
796
+ oldValue,
797
+ source,
798
+ timestamp: Date.now()
799
+ });
800
+ }
801
+ }
802
+ }
803
+ };
804
+