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,259 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ /**
6
+ * Default initialization vector (IV) for AES encryption.
7
+ *
8
+ * Meross devices require a zero-filled IV rather than a random IV. This is a
9
+ * protocol requirement that must be followed for compatibility with Meross firmware.
10
+ *
11
+ * @constant {Buffer}
12
+ * @private
13
+ */
14
+ const DEFAULT_IV = Buffer.from('0000000000000000', 'utf8');
15
+
16
+ /**
17
+ * Derives the encryption key from device UUID, Meross key, and MAC address.
18
+ *
19
+ * The Meross protocol requires a specific key derivation algorithm that combines
20
+ * portions of the device UUID, Meross cloud key, and MAC address. The specific
21
+ * substring ranges used here match the Meross firmware implementation.
22
+ *
23
+ * MD5 is used because it's part of the Meross protocol specification, despite
24
+ * being cryptographically weak. The hex digest is encoded as UTF-8 bytes to
25
+ * produce a 32-byte key suitable for AES-256-CBC encryption.
26
+ *
27
+ * @param {string} uuid - Device UUID (full UUID string)
28
+ * @param {string} mrskey - Meross cloud key (from device configuration)
29
+ * @param {string} mac - Device MAC address
30
+ * @returns {Buffer} 32-byte encryption key (MD5 hex string encoded as UTF-8 bytes) for AES-256-CBC
31
+ * @private
32
+ */
33
+ function _deriveEncryptionKey(uuid, mrskey, mac) {
34
+ const strtohash = uuid.substring(3, 22) +
35
+ mrskey.substring(1, 9) +
36
+ mac +
37
+ mrskey.substring(10, 28);
38
+ const hash = crypto.createHash('md5').update(strtohash).digest('hex');
39
+ return Buffer.from(hash, 'utf8');
40
+ }
41
+
42
+ /**
43
+ * Pads data to 16-byte blocks with zero bytes.
44
+ *
45
+ * AES encryption requires data to be aligned to 16-byte block boundaries.
46
+ * Meross devices use zero padding rather than standard PKCS7 padding, so
47
+ * zero bytes are appended until the data length is a multiple of 16.
48
+ *
49
+ * @param {Buffer} data - Data to pad
50
+ * @returns {Buffer} Padded data aligned to 16-byte blocks
51
+ * @private
52
+ */
53
+ function _padTo16Bytes(data) {
54
+ const blockSize = 16;
55
+ const padLength = blockSize - (data.length % blockSize);
56
+ const padding = Buffer.alloc(padLength, 0);
57
+ return Buffer.concat([data, padding]);
58
+ }
59
+
60
+ /**
61
+ * Encrypts message data using AES-256-CBC encryption.
62
+ *
63
+ * Encrypts message data for communication with Meross devices. The data is padded
64
+ * to 16-byte blocks using zero padding, then encrypted with AES-256-CBC using a
65
+ * zero-filled IV. Auto-padding is disabled because manual zero padding is used
66
+ * instead of the default PKCS7 padding.
67
+ *
68
+ * @param {Buffer|string} messageData - Message data to encrypt. Can be a Buffer or a JSON string.
69
+ * @param {Buffer} key - 32-byte encryption key (MD5 hex string encoded as UTF-8).
70
+ * @returns {string} Base64-encoded encrypted message
71
+ * @private
72
+ */
73
+ function _encrypt(messageData, key) {
74
+ let dataBuffer;
75
+ if (typeof messageData === 'string') {
76
+ dataBuffer = Buffer.from(messageData, 'utf8');
77
+ } else {
78
+ dataBuffer = messageData;
79
+ }
80
+
81
+ const paddedData = _padTo16Bytes(dataBuffer);
82
+
83
+ const cipher = crypto.createCipheriv('aes-256-cbc', key, DEFAULT_IV);
84
+ cipher.setAutoPadding(false);
85
+
86
+ let encrypted = cipher.update(paddedData);
87
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
88
+
89
+ return encrypted.toString('base64');
90
+ }
91
+
92
+ /**
93
+ * Decrypts encrypted message data using AES-256-CBC decryption.
94
+ *
95
+ * Decrypts message data received from Meross devices. The data is decoded from
96
+ * base64, decrypted using AES-256-CBC with a zero-filled IV, and zero padding
97
+ * is removed from the result. Auto-padding is disabled because manual zero padding
98
+ * removal is required instead of standard PKCS7 unpadding.
99
+ *
100
+ * @param {string|Buffer} encryptedData - Base64-encoded encrypted data (as string) or encrypted Buffer
101
+ * @param {Buffer} key - 32-byte encryption key (MD5 hex string encoded as UTF-8).
102
+ * @returns {Buffer} Decrypted data as Buffer
103
+ * @private
104
+ */
105
+ function _decrypt(encryptedData, key) {
106
+ let encryptedBuffer;
107
+ if (typeof encryptedData === 'string') {
108
+ encryptedBuffer = Buffer.from(encryptedData, 'base64');
109
+ } else {
110
+ encryptedBuffer = encryptedData;
111
+ }
112
+
113
+ const decipher = crypto.createDecipheriv('aes-256-cbc', key, DEFAULT_IV);
114
+ decipher.setAutoPadding(false);
115
+
116
+ let decrypted = decipher.update(encryptedBuffer);
117
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
118
+
119
+ // Remove zero padding by finding the last non-zero byte
120
+ let result = decrypted;
121
+ let lastNonZero = result.length - 1;
122
+ while (lastNonZero >= 0 && result[lastNonZero] === 0) {
123
+ lastNonZero--;
124
+ }
125
+ if (lastNonZero < result.length - 1) {
126
+ result = result.slice(0, lastNonZero + 1);
127
+ }
128
+
129
+ return result;
130
+ }
131
+
132
+ /**
133
+ * Encryption feature module.
134
+ * Handles encryption key management and message encryption/decryption for devices that support it.
135
+ */
136
+ module.exports = {
137
+ /**
138
+ * Initializes encryption support state.
139
+ *
140
+ * Called during device construction or when abilities are updated to ensure encryption
141
+ * state variables are properly initialized.
142
+ *
143
+ * @private
144
+ */
145
+ _initializeEncryption() {
146
+ if (this._encryptionKey === undefined) {
147
+ this._encryptionKey = null;
148
+ }
149
+ if (this._supportsEncryption === undefined) {
150
+ this._supportsEncryption = false;
151
+ }
152
+ },
153
+
154
+ /**
155
+ * Checks if the device supports encryption.
156
+ *
157
+ * Encryption support is automatically detected from device abilities when abilities are updated.
158
+ *
159
+ * @returns {boolean} True if device supports encryption, false otherwise
160
+ */
161
+ supportEncryption() {
162
+ this._initializeEncryption();
163
+ return this._supportsEncryption;
164
+ },
165
+
166
+ /**
167
+ * Checks if the encryption key has been set for this device.
168
+ *
169
+ * @returns {boolean} True if encryption key is set, false otherwise
170
+ * @see setEncryptionKey
171
+ */
172
+ isEncryptionKeySet() {
173
+ this._initializeEncryption();
174
+ return this._encryptionKey !== null;
175
+ },
176
+
177
+ /**
178
+ * Sets the encryption key for this device.
179
+ *
180
+ * The encryption key is derived from the device UUID, Meross key, and MAC address using
181
+ * a device-specific algorithm. This is typically called automatically when encryption
182
+ * support is detected and all required data is available.
183
+ *
184
+ * @param {string} uuid - Device UUID
185
+ * @param {string} mrskey - Meross key from cloud instance
186
+ * @param {string} mac - Device MAC address
187
+ */
188
+ setEncryptionKey(uuid, mrskey, mac) {
189
+ this._initializeEncryption();
190
+ this._encryptionKey = _deriveEncryptionKey(uuid, mrskey, mac);
191
+ this._macAddress = mac;
192
+ },
193
+
194
+ /**
195
+ * Encrypts a message using the device's encryption key.
196
+ *
197
+ * @param {Object|string|Buffer} messageData - Message data to encrypt
198
+ * @returns {string} Base64-encoded encrypted message
199
+ * @throws {import('../../model/exception').CommandError} If encryption key is not set
200
+ * @see decryptMessage
201
+ * @see isEncryptionKeySet
202
+ */
203
+ encryptMessage(messageData) {
204
+ if (!this.isEncryptionKeySet()) {
205
+ const { CommandError } = require('../../model/exception');
206
+ throw new CommandError('Encryption key is not set! Please invoke setEncryptionKey first.', null, this.uuid);
207
+ }
208
+ return _encrypt(messageData, this._encryptionKey);
209
+ },
210
+
211
+ /**
212
+ * Decrypts an encrypted message using the device's encryption key.
213
+ *
214
+ * @param {string|Buffer} encryptedData - Encrypted message data to decrypt
215
+ * @returns {Buffer} Decrypted message data
216
+ * @throws {import('../../model/exception').CommandError} If encryption key is not set
217
+ * @see encryptMessage
218
+ * @see isEncryptionKeySet
219
+ */
220
+ decryptMessage(encryptedData) {
221
+ if (!this.isEncryptionKeySet()) {
222
+ const { CommandError } = require('../../model/exception');
223
+ throw new CommandError('Encryption key is not set! Please invoke setEncryptionKey first.', null, this.uuid);
224
+ }
225
+ return _decrypt(encryptedData, this._encryptionKey);
226
+ },
227
+
228
+ /**
229
+ * Updates device abilities and detects encryption support.
230
+ *
231
+ * Checks for the Appliance.Encrypt.ECDHE namespace in abilities to determine if
232
+ * the device supports encryption.
233
+ *
234
+ * @param {Object} abilities - Device abilities object
235
+ * @private
236
+ */
237
+ _updateAbilitiesWithEncryption(abilities) {
238
+ this._initializeEncryption();
239
+ this._abilities = abilities;
240
+ this._supportsEncryption = abilities && typeof abilities === 'object' &&
241
+ 'Appliance.Encrypt.ECDHE' in abilities;
242
+ },
243
+
244
+ /**
245
+ * Updates MAC address and automatically sets encryption key if conditions are met.
246
+ *
247
+ * If encryption is supported, the key is not yet set, and all required data (MAC address,
248
+ * cloud instance key) is available, the encryption key is automatically derived and set.
249
+ *
250
+ * @param {string} mac - Device MAC address
251
+ * @private
252
+ */
253
+ _updateMacAddressWithEncryption(mac) {
254
+ this._macAddress = mac;
255
+ if (this._supportsEncryption && !this.isEncryptionKeySet() && this._macAddress && this.cloudInst && this.cloudInst.key) {
256
+ this.setEncryptionKey(this.uuid, this.cloudInst.key, this._macAddress);
257
+ }
258
+ }
259
+ };
@@ -0,0 +1,337 @@
1
+ 'use strict';
2
+
3
+ const GarageDoorState = require('../../model/states/garage-door-state');
4
+ const { normalizeChannel } = require('../../utilities/options');
5
+
6
+ /**
7
+ * Garage door feature module.
8
+ * Provides control over garage door open/close state and configuration settings.
9
+ */
10
+ module.exports = {
11
+ /**
12
+ * Controls the garage door state (open/close).
13
+ *
14
+ * Automatically includes the device UUID in the payload.
15
+ *
16
+ * @param {Object} options - Garage door options
17
+ * @param {number} [options.channel=0] - Channel to control (default: 0)
18
+ * @param {boolean} options.open - True to open, false to close
19
+ * @returns {Promise<Object>} Response from the device containing the updated state
20
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
21
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
22
+ */
23
+ async setGarageDoor(options = {}) {
24
+ if (options.open === undefined) {
25
+ throw new Error('open is required');
26
+ }
27
+ const channel = normalizeChannel(options);
28
+ const payload = { 'state': { channel, 'open': options.open ? 1 : 0, 'uuid': this.uuid } };
29
+ const response = await this.publishMessage('SET', 'Appliance.GarageDoor.State', payload);
30
+
31
+ if (response && response.state) {
32
+ this._updateGarageDoorState(response.state, 'response');
33
+ this._lastFullUpdateTimestamp = Date.now();
34
+ } else {
35
+ this._updateGarageDoorState([{ channel, open: options.open ? 1 : 0 }], 'response');
36
+ this._lastFullUpdateTimestamp = Date.now();
37
+ }
38
+
39
+ return response;
40
+ },
41
+
42
+ /**
43
+ * Gets the current garage door state from the device.
44
+ *
45
+ * Use {@link getCachedGarageDoorState} to get cached state without making a request.
46
+ *
47
+ * @param {Object} [options={}] - Get options
48
+ * @param {number} [options.channel=0] - Channel to get state for (default: 0, use 0xffff for all channels)
49
+ * @returns {Promise<Object>} Response containing garage door state with `state` array
50
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
51
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
52
+ */
53
+ async getGarageDoorState(options = {}) {
54
+ const channel = normalizeChannel(options);
55
+ const payload = { 'state': { channel } };
56
+ const response = await this.publishMessage('GET', 'Appliance.GarageDoor.State', payload);
57
+ if (response && response.state) {
58
+ this._updateGarageDoorState(response.state, 'response');
59
+ this._lastFullUpdateTimestamp = Date.now();
60
+ }
61
+ return response;
62
+ },
63
+
64
+ /**
65
+ * Gets the garage door multiple configuration state.
66
+ *
67
+ * Retrieves configuration for all channels. Use {@link getGarageDoorConfig} (getter) to get
68
+ * cached configuration for a specific channel.
69
+ * @param {Object} [options={}] - Get options
70
+ * @returns {Promise<Object>} Response containing garage door config with `config` array
71
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
72
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
73
+ */
74
+ async getGarageDoorMultipleState(_options = {}) {
75
+ const response = await this.publishMessage('GET', 'Appliance.GarageDoor.MultipleConfig', {});
76
+ if (response && response.config) {
77
+ this._updateGarageDoorConfig(response.config);
78
+ this._lastFullUpdateTimestamp = Date.now();
79
+ }
80
+ return response;
81
+ },
82
+
83
+ /**
84
+ * Gets the garage door multiple configuration (alias for getGarageDoorMultipleState).
85
+ *
86
+ * @param {Object} [options={}] - Get options
87
+ * @see getGarageDoorMultipleState
88
+ * @returns {Promise<Object>} Response containing garage door config with `config` array
89
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
90
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
91
+ */
92
+ async getGarageDoorMultipleConfig(_options = {}) {
93
+ return await this.getGarageDoorMultipleState(_options);
94
+ },
95
+
96
+ /**
97
+ * Gets the garage door configuration from the device.
98
+ *
99
+ * Note: This method name conflicts with the getter method {@link getGarageDoorConfig} that
100
+ * returns cached config. Use {@link getGarageDoorMultipleState} to get config from device.
101
+ * @param {Object} [options={}] - Get options
102
+ * @returns {Promise<Object>} Response containing garage door configuration
103
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
104
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
105
+ */
106
+ async getGarageDoorConfig(_options = {}) {
107
+ return await this.publishMessage('GET', 'Appliance.GarageDoor.Config', {});
108
+ },
109
+
110
+ /**
111
+ * Controls the garage door configuration.
112
+ *
113
+ * @param {Object} options - Garage door config options
114
+ * @param {Object} [options.configData] - Configuration data object (if provided, used directly)
115
+ * @param {number} [options.signalDuration] - Signal duration in milliseconds
116
+ * @param {boolean} [options.buzzerEnable] - Enable/disable buzzer
117
+ * @param {number} [options.doorOpenDuration] - Door open duration in milliseconds
118
+ * @param {number} [options.doorCloseDuration] - Door close duration in milliseconds
119
+ * @returns {Promise<Object>} Response from the device
120
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
121
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
122
+ */
123
+ async setGarageDoorConfig(options = {}) {
124
+ let configData;
125
+ if (options.configData) {
126
+ configData = options.configData;
127
+ } else {
128
+ configData = {
129
+ signalDuration: options.signalDuration,
130
+ buzzerEnable: options.buzzerEnable,
131
+ doorOpenDuration: options.doorOpenDuration,
132
+ doorCloseDuration: options.doorCloseDuration
133
+ };
134
+ // Remove undefined values
135
+ Object.keys(configData).forEach(key => {
136
+ if (configData[key] === undefined) {
137
+ delete configData[key];
138
+ }
139
+ });
140
+ }
141
+ const payload = { config: configData };
142
+ return await this.publishMessage('SET', 'Appliance.GarageDoor.Config', payload);
143
+ },
144
+
145
+ /**
146
+ * Gets the cached garage door state for the specified channel.
147
+ *
148
+ * Returns cached state without making a request. Use {@link getGarageDoorState} to fetch
149
+ * fresh state from the device. State is automatically updated when commands are sent or
150
+ * push notifications are received.
151
+ *
152
+ * @param {number} [channel=0] - Channel to get state for (default: 0)
153
+ * @returns {import('../lib/model/states/garage-door-state').GarageDoorState|undefined} Cached garage door state or undefined if not available
154
+ * @throws {Error} If state has not been initialized (call refreshState() first)
155
+ */
156
+ getCachedGarageDoorState(channel = 0) {
157
+ this.validateState();
158
+ return this._garageDoorStateByChannel.get(channel);
159
+ },
160
+
161
+ /**
162
+ * Checks if the garage door is opened for the specified channel.
163
+ *
164
+ * Uses cached state. Ensure state is initialized with {@link refreshState} first.
165
+ *
166
+ * @param {number} [channel=0] - Channel to check (default: 0)
167
+ * @returns {boolean|undefined} True if open, false if closed, undefined if not available
168
+ * @throws {Error} If state has not been initialized (call refreshState() first)
169
+ * @see isGarageDoorClosed
170
+ */
171
+ isGarageDoorOpened(channel = 0) {
172
+ this.validateState();
173
+ const state = this._garageDoorStateByChannel.get(channel);
174
+ if (state) {
175
+ return state.isOpen;
176
+ }
177
+ return undefined;
178
+ },
179
+
180
+ /**
181
+ * Checks if the garage door is closed for the specified channel.
182
+ *
183
+ * Uses cached state. Ensure state is initialized with {@link refreshState} first.
184
+ *
185
+ * @param {number} [channel=0] - Channel to check (default: 0)
186
+ * @returns {boolean|undefined} True if closed, false if open, undefined if not available
187
+ * @throws {Error} If state has not been initialized (call refreshState() first)
188
+ * @see isGarageDoorOpened
189
+ */
190
+ isGarageDoorClosed(channel = 0) {
191
+ this.validateState();
192
+ const isOpen = this.isGarageDoorOpened(channel);
193
+ if (isOpen === undefined) {
194
+ return undefined;
195
+ }
196
+ return !isOpen;
197
+ },
198
+
199
+ /**
200
+ * Gets the garage door configuration for the specified channel (cached).
201
+ *
202
+ * Returns cached configuration without making a request. Use {@link getGarageDoorMultipleState}
203
+ * to fetch fresh configuration from the device.
204
+ *
205
+ * @param {number} [channel=0] - Channel to get config for (default: 0)
206
+ * @returns {Object|undefined} Garage door config or undefined if not available
207
+ * @throws {Error} If state has not been initialized (call refreshState() first)
208
+ */
209
+ getGarageDoorConfig(channel = 0) {
210
+ this.validateState();
211
+ return this._garageDoorConfigByChannel.get(channel);
212
+ },
213
+
214
+ /**
215
+ * Opens the garage door for the specified channel.
216
+ *
217
+ * Convenience method that calls {@link setGarageDoor} with `open = true`.
218
+ *
219
+ * @param {Object} [options={}] - Open options
220
+ * @param {number} [options.channel=0] - Channel to control (default: 0)
221
+ * @returns {Promise<Object>} Response from the device
222
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
223
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
224
+ * @see closeGarageDoor
225
+ * @see toggleGarageDoor
226
+ */
227
+ async openGarageDoor(options = {}) {
228
+ return await this.setGarageDoor({ ...options, open: true });
229
+ },
230
+
231
+ /**
232
+ * Closes the garage door for the specified channel.
233
+ *
234
+ * Convenience method that calls {@link setGarageDoor} with `open = false`.
235
+ *
236
+ * @param {Object} [options={}] - Close options
237
+ * @param {number} [options.channel=0] - Channel to control (default: 0)
238
+ * @returns {Promise<Object>} Response from the device
239
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
240
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
241
+ * @see openGarageDoor
242
+ * @see toggleGarageDoor
243
+ */
244
+ async closeGarageDoor(options = {}) {
245
+ return await this.setGarageDoor({ ...options, open: false });
246
+ },
247
+
248
+ /**
249
+ * Toggles the garage door state for the specified channel.
250
+ *
251
+ * Opens if closed, closes if open. Uses cached state to determine current state.
252
+ *
253
+ * @param {Object} [options={}] - Toggle options
254
+ * @param {number} [options.channel=0] - Channel to control (default: 0)
255
+ * @returns {Promise<Object>} Response from the device
256
+ * @throws {import('../lib/errors/errors').UnconnectedError} If device is not connected
257
+ * @throws {import('../lib/errors/errors').CommandTimeoutError} If command times out
258
+ * @throws {Error} If state has not been initialized (call refreshState() first)
259
+ * @see openGarageDoor
260
+ * @see closeGarageDoor
261
+ */
262
+ async toggleGarageDoor(options = {}) {
263
+ const channel = normalizeChannel(options);
264
+ const isOpen = this.isGarageDoorOpened(channel);
265
+ const newState = isOpen === true ? false : true;
266
+ return await this.setGarageDoor({ channel, open: newState });
267
+ },
268
+
269
+ /**
270
+ * Updates the cached garage door state from state data.
271
+ *
272
+ * Called automatically when garage door push notifications are received or commands complete.
273
+ * Handles both single objects and arrays of state data.
274
+ *
275
+ * @param {Object|Array} stateData - State data (single object or array)
276
+ * @param {string} [source='response'] - Source of the update ('push' | 'poll' | 'response')
277
+ * @private
278
+ */
279
+ _updateGarageDoorState(stateData, source = 'response') {
280
+ if (!stateData) {return;}
281
+
282
+ const stateArray = Array.isArray(stateData) ? stateData : [stateData];
283
+
284
+ for (const stateItem of stateArray) {
285
+ const channelIndex = stateItem.channel;
286
+ if (channelIndex === undefined || channelIndex === null) {continue;}
287
+
288
+ const oldState = this._garageDoorStateByChannel.get(channelIndex);
289
+ const oldValue = oldState ? {
290
+ isOpen: oldState.isOpen
291
+ } : undefined;
292
+
293
+ let state = this._garageDoorStateByChannel.get(channelIndex);
294
+ if (!state) {
295
+ state = new GarageDoorState(stateItem);
296
+ this._garageDoorStateByChannel.set(channelIndex, state);
297
+ } else {
298
+ state.update(stateItem);
299
+ }
300
+
301
+ const newValue = { isOpen: state.isOpen };
302
+ if (oldValue === undefined || oldValue.isOpen !== state.isOpen) {
303
+ this.emit('stateChange', {
304
+ type: 'garageDoor',
305
+ channel: channelIndex,
306
+ value: newValue,
307
+ oldValue,
308
+ source,
309
+ timestamp: Date.now()
310
+ });
311
+ }
312
+ }
313
+ },
314
+
315
+ /**
316
+ * Updates the cached garage door configuration from config data.
317
+ *
318
+ * Called automatically when garage door configuration responses are received.
319
+ * Handles both single objects and arrays of config data.
320
+ *
321
+ * @param {Object|Array} configData - Config data (single object or array)
322
+ * @private
323
+ */
324
+ _updateGarageDoorConfig(configData) {
325
+ if (!configData) {return;}
326
+
327
+ const configArray = Array.isArray(configData) ? configData : [configData];
328
+
329
+ for (const configItem of configArray) {
330
+ const channelIndex = configItem.channel;
331
+ if (channelIndex === undefined || channelIndex === null) {continue;}
332
+
333
+ this._garageDoorConfigByChannel.set(channelIndex, configItem);
334
+ }
335
+ }
336
+ };
337
+