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.
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/index.d.ts +2344 -0
- package/index.js +131 -0
- package/lib/controller/device.js +1317 -0
- package/lib/controller/features/alarm-feature.js +89 -0
- package/lib/controller/features/child-lock-feature.js +61 -0
- package/lib/controller/features/config-feature.js +54 -0
- package/lib/controller/features/consumption-feature.js +210 -0
- package/lib/controller/features/control-feature.js +62 -0
- package/lib/controller/features/diffuser-feature.js +411 -0
- package/lib/controller/features/digest-timer-feature.js +22 -0
- package/lib/controller/features/digest-trigger-feature.js +22 -0
- package/lib/controller/features/dnd-feature.js +79 -0
- package/lib/controller/features/electricity-feature.js +144 -0
- package/lib/controller/features/encryption-feature.js +259 -0
- package/lib/controller/features/garage-feature.js +337 -0
- package/lib/controller/features/hub-feature.js +687 -0
- package/lib/controller/features/light-feature.js +408 -0
- package/lib/controller/features/presence-sensor-feature.js +297 -0
- package/lib/controller/features/roller-shutter-feature.js +456 -0
- package/lib/controller/features/runtime-feature.js +74 -0
- package/lib/controller/features/screen-feature.js +67 -0
- package/lib/controller/features/sensor-history-feature.js +47 -0
- package/lib/controller/features/smoke-config-feature.js +50 -0
- package/lib/controller/features/spray-feature.js +166 -0
- package/lib/controller/features/system-feature.js +269 -0
- package/lib/controller/features/temp-unit-feature.js +55 -0
- package/lib/controller/features/thermostat-feature.js +804 -0
- package/lib/controller/features/timer-feature.js +507 -0
- package/lib/controller/features/toggle-feature.js +223 -0
- package/lib/controller/features/trigger-feature.js +333 -0
- package/lib/controller/hub-device.js +185 -0
- package/lib/controller/subdevice.js +1537 -0
- package/lib/device-factory.js +463 -0
- package/lib/error-budget.js +138 -0
- package/lib/http-api.js +766 -0
- package/lib/manager.js +1609 -0
- package/lib/model/channel-info.js +79 -0
- package/lib/model/constants.js +119 -0
- package/lib/model/enums.js +819 -0
- package/lib/model/exception.js +363 -0
- package/lib/model/http/device.js +215 -0
- package/lib/model/http/error-codes.js +121 -0
- package/lib/model/http/exception.js +151 -0
- package/lib/model/http/subdevice.js +133 -0
- package/lib/model/push/alarm.js +112 -0
- package/lib/model/push/bind.js +97 -0
- package/lib/model/push/common.js +282 -0
- package/lib/model/push/diffuser-light.js +100 -0
- package/lib/model/push/diffuser-spray.js +83 -0
- package/lib/model/push/factory.js +229 -0
- package/lib/model/push/generic.js +115 -0
- package/lib/model/push/hub-battery.js +59 -0
- package/lib/model/push/hub-mts100-all.js +64 -0
- package/lib/model/push/hub-mts100-mode.js +59 -0
- package/lib/model/push/hub-mts100-temperature.js +62 -0
- package/lib/model/push/hub-online.js +59 -0
- package/lib/model/push/hub-sensor-alert.js +61 -0
- package/lib/model/push/hub-sensor-all.js +59 -0
- package/lib/model/push/hub-sensor-smoke.js +110 -0
- package/lib/model/push/hub-sensor-temphum.js +62 -0
- package/lib/model/push/hub-subdevicelist.js +50 -0
- package/lib/model/push/hub-togglex.js +60 -0
- package/lib/model/push/index.js +81 -0
- package/lib/model/push/online.js +53 -0
- package/lib/model/push/presence-study.js +61 -0
- package/lib/model/push/sensor-latestx.js +106 -0
- package/lib/model/push/timerx.js +63 -0
- package/lib/model/push/togglex.js +78 -0
- package/lib/model/push/triggerx.js +62 -0
- package/lib/model/push/unbind.js +34 -0
- package/lib/model/push/water-leak.js +107 -0
- package/lib/model/states/diffuser-light-state.js +119 -0
- package/lib/model/states/diffuser-spray-state.js +58 -0
- package/lib/model/states/garage-door-state.js +71 -0
- package/lib/model/states/index.js +38 -0
- package/lib/model/states/light-state.js +134 -0
- package/lib/model/states/presence-sensor-state.js +239 -0
- package/lib/model/states/roller-shutter-state.js +82 -0
- package/lib/model/states/spray-state.js +58 -0
- package/lib/model/states/thermostat-state.js +297 -0
- package/lib/model/states/timer-state.js +192 -0
- package/lib/model/states/toggle-state.js +105 -0
- package/lib/model/states/trigger-state.js +155 -0
- package/lib/subscription.js +587 -0
- package/lib/utilities/conversion.js +62 -0
- package/lib/utilities/debug.js +165 -0
- package/lib/utilities/mqtt.js +152 -0
- package/lib/utilities/network.js +53 -0
- package/lib/utilities/options.js +64 -0
- package/lib/utilities/request-queue.js +161 -0
- package/lib/utilities/ssid.js +37 -0
- package/lib/utilities/state-changes.js +66 -0
- package/lib/utilities/stats.js +687 -0
- package/lib/utilities/timer.js +310 -0
- package/lib/utilities/trigger.js +286 -0
- 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
|
+
|