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,1537 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { MerossDevice } = require('./device');
|
|
4
|
+
const { OnlineStatus, SmokeAlarmStatus } = require('../model/enums');
|
|
5
|
+
// DeviceRegistry is nested in MerossManager but exported separately to avoid circular dependencies
|
|
6
|
+
const { MerossManager } = require('../manager');
|
|
7
|
+
const { UnknownDeviceTypeError, CommandError } = require('../model/exception');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Base class for all Meross subdevices.
|
|
11
|
+
*
|
|
12
|
+
* Subdevices connect through a hub device and route all commands through the hub.
|
|
13
|
+
* They cannot communicate directly with the Meross cloud, so all MQTT messages and
|
|
14
|
+
* HTTP requests must go through the parent hub device.
|
|
15
|
+
*
|
|
16
|
+
* Subdevices include sensors (temperature/humidity, water leak, smoke), thermostat
|
|
17
|
+
* valves, and other devices that connect to hub devices rather than directly to WiFi.
|
|
18
|
+
*
|
|
19
|
+
* @class MerossSubDevice
|
|
20
|
+
* @extends MerossDevice
|
|
21
|
+
* @example
|
|
22
|
+
* // Subdevices are typically created automatically when hubs are discovered
|
|
23
|
+
* const hub = devices.find(d => d instanceof MerossHubDevice);
|
|
24
|
+
* const subdevices = hub.getSubdevices();
|
|
25
|
+
*
|
|
26
|
+
* // Access subdevice properties
|
|
27
|
+
* const sensor = subdevices[0];
|
|
28
|
+
* console.log(`Subdevice ID: ${sensor.subdeviceId}`);
|
|
29
|
+
* console.log(`Hub UUID: ${sensor.hub.uuid}`);
|
|
30
|
+
* console.log(`Name: ${sensor.name}`);
|
|
31
|
+
*/
|
|
32
|
+
class MerossSubDevice extends MerossDevice {
|
|
33
|
+
/**
|
|
34
|
+
* Creates a new MerossSubDevice instance
|
|
35
|
+
* @param {string} hubDeviceUuid - UUID of the hub device this subdevice belongs to
|
|
36
|
+
* @param {string} subdeviceId - Subdevice ID
|
|
37
|
+
* @param {Object} manager - The MerossCloud manager instance
|
|
38
|
+
* @param {Object} [kwargs] - Additional subdevice information
|
|
39
|
+
* @param {string} [kwargs.subDeviceType] - Subdevice type (or type)
|
|
40
|
+
* @param {string} [kwargs.subDeviceName] - Subdevice name (or name)
|
|
41
|
+
* @throws {Error} If hub device is not found or subdevice ID is missing
|
|
42
|
+
*/
|
|
43
|
+
constructor(hubDeviceUuid, subdeviceId, manager, kwargs = {}) {
|
|
44
|
+
// eslint-disable-next-line camelcase
|
|
45
|
+
const hubs = manager.findDevices({ device_uuids: [hubDeviceUuid] });
|
|
46
|
+
if (!hubs || hubs.length < 1) {
|
|
47
|
+
throw new UnknownDeviceTypeError(`Specified hub device ${hubDeviceUuid} is not present`);
|
|
48
|
+
}
|
|
49
|
+
const hub = hubs[0];
|
|
50
|
+
|
|
51
|
+
super(manager, hubDeviceUuid, hub.mqttHost, hub.mqttPort);
|
|
52
|
+
|
|
53
|
+
this._hub = hub;
|
|
54
|
+
this._subdeviceId = subdeviceId;
|
|
55
|
+
this._type = kwargs.subDeviceType || kwargs.type;
|
|
56
|
+
this._name = kwargs.subDeviceName || kwargs.name;
|
|
57
|
+
|
|
58
|
+
if (!this._subdeviceId) {
|
|
59
|
+
throw new UnknownDeviceTypeError('Subdevice ID is required');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Subdevices share hub's network configuration since they communicate through the hub
|
|
63
|
+
this._mqttHost = hub.mqttHost;
|
|
64
|
+
this._mqttPort = hub.mqttPort;
|
|
65
|
+
this._lanIp = hub.lanIp;
|
|
66
|
+
|
|
67
|
+
this._onlineStatus = OnlineStatus.UNKNOWN;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Gets the subdevice ID.
|
|
72
|
+
*
|
|
73
|
+
* This is the unique identifier for this subdevice within the hub.
|
|
74
|
+
*
|
|
75
|
+
* @returns {string} Subdevice ID
|
|
76
|
+
*/
|
|
77
|
+
get subdeviceId() {
|
|
78
|
+
return this._subdeviceId;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Gets the hub device this subdevice belongs to.
|
|
83
|
+
*
|
|
84
|
+
* All subdevice commands are routed through this hub device.
|
|
85
|
+
*
|
|
86
|
+
* @returns {MerossHubDevice} Hub device instance
|
|
87
|
+
*/
|
|
88
|
+
get hub() {
|
|
89
|
+
return this._hub;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Gets the subdevice type
|
|
94
|
+
* @returns {string|undefined} Subdevice type or undefined if not available
|
|
95
|
+
*/
|
|
96
|
+
get type() {
|
|
97
|
+
return this._type;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Gets the subdevice name
|
|
102
|
+
* @returns {string} Subdevice name or subdevice ID if name not available
|
|
103
|
+
*/
|
|
104
|
+
get name() {
|
|
105
|
+
return this._name || this._subdeviceId;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Gets the UUID (uses hub's UUID for MQTT routing).
|
|
110
|
+
*
|
|
111
|
+
* Subdevices use their parent hub's UUID for MQTT message routing since
|
|
112
|
+
* they cannot communicate directly with the Meross cloud.
|
|
113
|
+
*
|
|
114
|
+
* @returns {string} Hub device UUID
|
|
115
|
+
*/
|
|
116
|
+
get uuid() {
|
|
117
|
+
return this._hub.uuid;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Gets the internal ID used for device registry.
|
|
122
|
+
*
|
|
123
|
+
* Generates and caches a composite ID combining hub UUID and subdevice ID on first
|
|
124
|
+
* access (format: #BASE:{hubUuid}#SUB:{subdeviceId}) to ensure unique identification
|
|
125
|
+
* in the device registry.
|
|
126
|
+
*
|
|
127
|
+
* @returns {string} Internal ID string
|
|
128
|
+
* @throws {Error} If hub UUID is missing
|
|
129
|
+
*/
|
|
130
|
+
get internalId() {
|
|
131
|
+
if (this._internalId) {
|
|
132
|
+
return this._internalId;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const hubUuid = this._hub.uuid || this._hub.dev?.uuid;
|
|
136
|
+
if (!hubUuid) {
|
|
137
|
+
throw new UnknownDeviceTypeError('Cannot generate internal ID: hub missing UUID');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this._internalId = MerossManager.DeviceRegistry.generateInternalId(hubUuid, true, hubUuid, this._subdeviceId);
|
|
141
|
+
return this._internalId;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Gets the online status of the subdevice.
|
|
146
|
+
*
|
|
147
|
+
* If the hub is offline, the subdevice is also considered offline since subdevices
|
|
148
|
+
* cannot communicate without the hub.
|
|
149
|
+
*
|
|
150
|
+
* @returns {number} Online status from OnlineStatus enum
|
|
151
|
+
*/
|
|
152
|
+
get onlineStatus() {
|
|
153
|
+
if (this._hub.onlineStatus !== OnlineStatus.ONLINE) {
|
|
154
|
+
return this._hub.onlineStatus;
|
|
155
|
+
}
|
|
156
|
+
return this._onlineStatus;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Refreshes the subdevice state through the hub.
|
|
161
|
+
*
|
|
162
|
+
* Delegates to the hub's refreshState() method, which updates both the hub and
|
|
163
|
+
* all its subdevices in a single operation. Subclasses may override to add
|
|
164
|
+
* subdevice-specific refresh logic.
|
|
165
|
+
*
|
|
166
|
+
* @returns {Promise<void>} Promise that resolves when state is refreshed
|
|
167
|
+
* @example
|
|
168
|
+
* await sensor.refreshState();
|
|
169
|
+
* // Sensor state is now updated
|
|
170
|
+
*/
|
|
171
|
+
async refreshState() {
|
|
172
|
+
await this._hub.refreshState();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Gets the cached battery level percentage for this subdevice.
|
|
177
|
+
*
|
|
178
|
+
* Returns the last known battery value without fetching from the device.
|
|
179
|
+
* Use getBattery() to fetch fresh battery data.
|
|
180
|
+
*
|
|
181
|
+
* @returns {number|null} Cached battery level (0-100), or null if not available
|
|
182
|
+
* @example
|
|
183
|
+
* const battery = sensor.getCachedBattery();
|
|
184
|
+
* if (battery !== null) {
|
|
185
|
+
* console.log(`Battery: ${battery}%`);
|
|
186
|
+
* }
|
|
187
|
+
*/
|
|
188
|
+
getCachedBattery() {
|
|
189
|
+
return this._battery !== undefined && this._battery !== null ? this._battery : null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Gets the battery level percentage for this subdevice by fetching from the device.
|
|
194
|
+
*
|
|
195
|
+
* Always fetches fresh battery data from the device and updates the cache.
|
|
196
|
+
* Use getCachedBattery() to get the last known value without making a request.
|
|
197
|
+
*
|
|
198
|
+
* @returns {Promise<number|null>} Promise that resolves with battery level (0-100), or null if not available
|
|
199
|
+
* @example
|
|
200
|
+
* const battery = await sensor.getBattery();
|
|
201
|
+
* if (battery !== null) {
|
|
202
|
+
* console.log(`Battery: ${battery}%`);
|
|
203
|
+
* }
|
|
204
|
+
*/
|
|
205
|
+
async getBattery() {
|
|
206
|
+
try {
|
|
207
|
+
if (typeof this._hub.getHubBattery !== 'function') {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
const response = await this._hub.getHubBattery();
|
|
211
|
+
if (response && response.battery && Array.isArray(response.battery)) {
|
|
212
|
+
const batteryData = response.battery.find(b => b.id === this._subdeviceId);
|
|
213
|
+
if (batteryData && batteryData.value !== undefined && batteryData.value !== null) {
|
|
214
|
+
// 0xFFFFFFFF and -1 are sentinel values indicating battery reporting is unsupported
|
|
215
|
+
if (batteryData.value === 0xFFFFFFFF || batteryData.value === -1) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
this._battery = batteryData.value;
|
|
219
|
+
return batteryData.value;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
const logger = this.cloudInst?.options?.logger || console.error;
|
|
225
|
+
logger(`Failed to get battery for subdevice ${this._subdeviceId}: ${error.message}`);
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Publishes a message by routing it through the hub device.
|
|
232
|
+
*
|
|
233
|
+
* All subdevice commands must be routed through the hub since subdevices cannot
|
|
234
|
+
* communicate directly with the Meross cloud. Delegates to the hub's publishMessage() method.
|
|
235
|
+
*
|
|
236
|
+
* @param {string} method - Message method ('GET', 'SET', etc.)
|
|
237
|
+
* @param {string} namespace - Message namespace (e.g., 'Appliance.Hub.Sensor.TempHum')
|
|
238
|
+
* @param {Object} payload - Message payload (must include subdevice ID)
|
|
239
|
+
* @param {number|null} [transportMode=null] - Transport mode from {@link TransportMode} enum
|
|
240
|
+
* @returns {Promise<Object>} Promise that resolves with the response payload
|
|
241
|
+
* @example
|
|
242
|
+
* // Get sensor data
|
|
243
|
+
* const response = await sensor.publishMessage(
|
|
244
|
+
* 'GET',
|
|
245
|
+
* 'Appliance.Hub.Sensor.TempHum',
|
|
246
|
+
* { id: sensor.subdeviceId }
|
|
247
|
+
* );
|
|
248
|
+
*/
|
|
249
|
+
async publishMessage(method, namespace, payload, transportMode = null) {
|
|
250
|
+
return await this._hub.publishMessage(method, namespace, payload, transportMode);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Maps numeric online status codes to OnlineStatus enum values.
|
|
255
|
+
*
|
|
256
|
+
* @private
|
|
257
|
+
* @param {number} statusValue - Numeric status code (0-3)
|
|
258
|
+
* @returns {number} OnlineStatus enum value
|
|
259
|
+
*/
|
|
260
|
+
_mapOnlineStatus(statusValue) {
|
|
261
|
+
const statusMap = {
|
|
262
|
+
0: OnlineStatus.NOT_ONLINE,
|
|
263
|
+
1: OnlineStatus.ONLINE,
|
|
264
|
+
2: OnlineStatus.OFFLINE,
|
|
265
|
+
3: OnlineStatus.UPGRADING
|
|
266
|
+
};
|
|
267
|
+
return statusMap[statusValue] ?? OnlineStatus.UNKNOWN;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Updates online status from notification data.
|
|
272
|
+
*
|
|
273
|
+
* Handles both data.online.status pattern (from Sensor.All, Mts100.All) and
|
|
274
|
+
* data.status pattern (from Hub.Online namespace) for compatibility with different
|
|
275
|
+
* notification formats.
|
|
276
|
+
*
|
|
277
|
+
* @private
|
|
278
|
+
* @param {Object} data - Notification data payload
|
|
279
|
+
* @returns {Object|null} Object with status and lastActiveTime if present, null otherwise
|
|
280
|
+
*/
|
|
281
|
+
_updateOnlineStatus(data) {
|
|
282
|
+
let statusValue;
|
|
283
|
+
let lastActiveTime;
|
|
284
|
+
|
|
285
|
+
if (data.online && data.online.status !== undefined) {
|
|
286
|
+
statusValue = data.online.status;
|
|
287
|
+
lastActiveTime = data.online.lastActiveTime;
|
|
288
|
+
} else if (data.status !== undefined) {
|
|
289
|
+
statusValue = data.status;
|
|
290
|
+
lastActiveTime = data.lastActiveTime;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (statusValue !== undefined) {
|
|
294
|
+
return {
|
|
295
|
+
status: this._mapOnlineStatus(statusValue),
|
|
296
|
+
lastActiveTime
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Handles notifications routed from hub to this subdevice.
|
|
305
|
+
*
|
|
306
|
+
* Subclasses override this method to process notification data and update cached state.
|
|
307
|
+
*
|
|
308
|
+
* @param {string} namespace - The namespace of the notification
|
|
309
|
+
* @param {object} data - The data payload for this subdevice
|
|
310
|
+
*/
|
|
311
|
+
async handleSubdeviceNotification(namespace, data) {
|
|
312
|
+
this.emit('subdeviceNotification', namespace, data);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Hub Temperature/Humidity Sensor subdevice (MS100, MS100F, MS130, etc.).
|
|
318
|
+
*
|
|
319
|
+
* This class represents temperature and humidity sensors that connect through
|
|
320
|
+
* a Meross hub device. It provides methods to read temperature and humidity values,
|
|
321
|
+
* access historical samples, and check sensor capabilities.
|
|
322
|
+
*
|
|
323
|
+
* @class HubTempHumSensor
|
|
324
|
+
* @extends MerossSubDevice
|
|
325
|
+
* @example
|
|
326
|
+
* const sensor = hub.getSubdevice('sensor123');
|
|
327
|
+
* if (sensor instanceof HubTempHumSensor) {
|
|
328
|
+
* // Get current temperature
|
|
329
|
+
* const temp = sensor.getLastSampledTemperature();
|
|
330
|
+
* console.log(`Temperature: ${temp}°C`);
|
|
331
|
+
*
|
|
332
|
+
* // Get current humidity
|
|
333
|
+
* const humidity = sensor.getLastSampledHumidity();
|
|
334
|
+
* console.log(`Humidity: ${humidity}%`);
|
|
335
|
+
*
|
|
336
|
+
* // Get temperature range
|
|
337
|
+
* const minTemp = sensor.getMinSupportedTemperature();
|
|
338
|
+
* const maxTemp = sensor.getMaxSupportedTemperature();
|
|
339
|
+
* console.log(`Range: ${minTemp}°C to ${maxTemp}°C`);
|
|
340
|
+
* }
|
|
341
|
+
*/
|
|
342
|
+
class HubTempHumSensor extends MerossSubDevice {
|
|
343
|
+
/**
|
|
344
|
+
* Creates a new HubTempHumSensor instance
|
|
345
|
+
* @param {string} hubDeviceUuid - UUID of the hub device
|
|
346
|
+
* @param {string} subdeviceId - Subdevice ID
|
|
347
|
+
* @param {Object} manager - The MerossCloud manager instance
|
|
348
|
+
* @param {Object} [kwargs] - Additional subdevice information
|
|
349
|
+
*/
|
|
350
|
+
constructor(hubDeviceUuid, subdeviceId, manager, kwargs = {}) {
|
|
351
|
+
super(hubDeviceUuid, subdeviceId, manager, kwargs);
|
|
352
|
+
|
|
353
|
+
this._temperature = {};
|
|
354
|
+
this._humidity = {};
|
|
355
|
+
this._battery = null;
|
|
356
|
+
this._lux = null;
|
|
357
|
+
this._samples = [];
|
|
358
|
+
this._lastSampledTime = null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Namespace handler registry for routing notifications
|
|
363
|
+
* @private
|
|
364
|
+
*/
|
|
365
|
+
static get NAMESPACE_HANDLERS() {
|
|
366
|
+
return [
|
|
367
|
+
{
|
|
368
|
+
namespace: 'Appliance.Hub.Sensor.All',
|
|
369
|
+
handler: '_handleSensorAll'
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
namespace: 'Appliance.Hub.Sensor.TempHum',
|
|
373
|
+
handler: '_handleSensorAll'
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
namespace: 'Appliance.Control.Sensor.LatestX',
|
|
377
|
+
handler: '_handleLatestX'
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
namespace: 'Appliance.Hub.Battery',
|
|
381
|
+
handler: '_handleBattery'
|
|
382
|
+
}
|
|
383
|
+
];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Handles subdevice-specific notifications from the hub.
|
|
388
|
+
*
|
|
389
|
+
* Routes notifications to appropriate handler methods based on namespace registry.
|
|
390
|
+
*
|
|
391
|
+
* @param {string} namespace - Notification namespace
|
|
392
|
+
* @param {Object} data - Notification data payload
|
|
393
|
+
*/
|
|
394
|
+
async handleSubdeviceNotification(namespace, data) {
|
|
395
|
+
await super.handleSubdeviceNotification(namespace, data);
|
|
396
|
+
|
|
397
|
+
const handler = HubTempHumSensor.NAMESPACE_HANDLERS.find(route => route.namespace === namespace);
|
|
398
|
+
if (handler && typeof this[handler.handler] === 'function') {
|
|
399
|
+
await this[handler.handler](data);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Handles Appliance.Hub.Sensor.All and Appliance.Hub.Sensor.TempHum namespaces.
|
|
405
|
+
*
|
|
406
|
+
* @private
|
|
407
|
+
* @param {Object} data - Notification data payload
|
|
408
|
+
*/
|
|
409
|
+
_handleSensorAll(data) {
|
|
410
|
+
const onlineStatus = this._updateOnlineStatus(data);
|
|
411
|
+
if (onlineStatus) {
|
|
412
|
+
this._onlineStatus = onlineStatus.status;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Merge to preserve existing properties like min/max temperature ranges
|
|
416
|
+
if (data.temperature) {
|
|
417
|
+
this._temperature = { ...this._temperature, ...data.temperature };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (data.humidity) {
|
|
421
|
+
this._humidity = { ...this._humidity, ...data.humidity };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (data.battery !== undefined && data.battery !== null) {
|
|
425
|
+
this._battery = data.battery;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (data.sample && Array.isArray(data.sample)) {
|
|
429
|
+
this._samples = this._parseSampleArray(data.sample);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (data.syncedTime) {
|
|
433
|
+
this._lastSampledTime = new Date(data.syncedTime * 1000);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Handles Appliance.Control.Sensor.LatestX namespace.
|
|
439
|
+
*
|
|
440
|
+
* @private
|
|
441
|
+
* @param {Object} data - Notification data payload
|
|
442
|
+
*/
|
|
443
|
+
_handleLatestX(data) {
|
|
444
|
+
if (!data.data) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const tempReading = this._extractLatestReading(data.data, 'temp');
|
|
449
|
+
if (tempReading) {
|
|
450
|
+
this._temperature.latest = tempReading.value;
|
|
451
|
+
if (tempReading.timestamp) {
|
|
452
|
+
this._temperature.latestSampleTime = tempReading.timestamp;
|
|
453
|
+
this._lastSampledTime = new Date(tempReading.timestamp * 1000);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const humiReading = this._extractLatestReading(data.data, 'humi');
|
|
458
|
+
if (humiReading) {
|
|
459
|
+
this._humidity.latest = humiReading.value;
|
|
460
|
+
if (humiReading.timestamp) {
|
|
461
|
+
this._humidity.latestSampleTime = humiReading.timestamp;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const lightReading = this._extractLatestReading(data.data, 'light');
|
|
466
|
+
if (lightReading) {
|
|
467
|
+
this._lux = lightReading.value;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Handles Appliance.Hub.Battery namespace.
|
|
473
|
+
*
|
|
474
|
+
* @private
|
|
475
|
+
* @param {Object} data - Notification data payload
|
|
476
|
+
*/
|
|
477
|
+
_handleBattery(data) {
|
|
478
|
+
// Filter out sentinel values (0xFFFFFFFF, -1) indicating battery reporting is unsupported
|
|
479
|
+
if (data.value !== undefined && data.value !== null &&
|
|
480
|
+
data.value !== 0xFFFFFFFF && data.value !== -1) {
|
|
481
|
+
this._battery = data.value;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Parses sample array into structured objects.
|
|
487
|
+
*
|
|
488
|
+
* @private
|
|
489
|
+
* @param {Array} samples - Array of sample data
|
|
490
|
+
* @returns {Array} Array of parsed sample objects
|
|
491
|
+
*/
|
|
492
|
+
_parseSampleArray(samples) {
|
|
493
|
+
return samples.map(sample => {
|
|
494
|
+
const [temp, hum, fromTs, toTs] = sample;
|
|
495
|
+
return {
|
|
496
|
+
fromTs,
|
|
497
|
+
toTs,
|
|
498
|
+
temperature: temp ? parseFloat(temp) / 10 : null,
|
|
499
|
+
humidity: hum ? parseFloat(hum) / 10 : null
|
|
500
|
+
};
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Extracts latest reading from LatestX data format.
|
|
506
|
+
*
|
|
507
|
+
* @private
|
|
508
|
+
* @param {Object} data - LatestX data object
|
|
509
|
+
* @param {string} type - Reading type ('temp', 'humi', 'light')
|
|
510
|
+
* @returns {Object|null} Reading object with value and timestamp, or null if not available
|
|
511
|
+
*/
|
|
512
|
+
_extractLatestReading(data, type) {
|
|
513
|
+
const readingArray = data[type];
|
|
514
|
+
if (!Array.isArray(readingArray) || readingArray.length === 0) {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const readingData = readingArray[0];
|
|
519
|
+
if (readingData.value === undefined || readingData.value === null) {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
value: readingData.value,
|
|
525
|
+
timestamp: readingData.timestamp
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Gets the last sampled temperature in Celsius.
|
|
531
|
+
*
|
|
532
|
+
* Temperature values are stored as tenths of degrees (×10). Handles edge case where
|
|
533
|
+
* some firmware versions double-scale values (×100), detected by values > 1000.
|
|
534
|
+
*
|
|
535
|
+
* @returns {number|null} Temperature in Celsius, or null if not available
|
|
536
|
+
* @example
|
|
537
|
+
* const temp = sensor.getLastSampledTemperature();
|
|
538
|
+
* if (temp !== null) {
|
|
539
|
+
* console.log(`Current temperature: ${temp}°C`);
|
|
540
|
+
* }
|
|
541
|
+
*/
|
|
542
|
+
getLastSampledTemperature() {
|
|
543
|
+
const temp = this._temperature.latest;
|
|
544
|
+
if (temp === undefined || temp === null) {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
const tempValue = parseFloat(temp);
|
|
548
|
+
if (tempValue > 1000) {
|
|
549
|
+
return tempValue / 100.0;
|
|
550
|
+
}
|
|
551
|
+
return tempValue / 10.0;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Gets the last sampled humidity percentage.
|
|
556
|
+
*
|
|
557
|
+
* @returns {number|null} Humidity percentage (0-100), or null if not available
|
|
558
|
+
* @example
|
|
559
|
+
* const humidity = sensor.getLastSampledHumidity();
|
|
560
|
+
* if (humidity !== null) {
|
|
561
|
+
* console.log(`Current humidity: ${humidity}%`);
|
|
562
|
+
* }
|
|
563
|
+
*/
|
|
564
|
+
getLastSampledHumidity() {
|
|
565
|
+
const hum = this._humidity.latest;
|
|
566
|
+
if (hum === undefined || hum === null) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
return parseFloat(hum) / 10.0;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Gets the timestamp of the last sample
|
|
574
|
+
* @returns {Date|null} Date object or null if not available
|
|
575
|
+
*/
|
|
576
|
+
getLastSampledTime() {
|
|
577
|
+
return this._lastSampledTime;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Gets the minimum supported temperature in Celsius
|
|
582
|
+
* @returns {number|null} Minimum temperature in Celsius or null if not available
|
|
583
|
+
*/
|
|
584
|
+
getMinSupportedTemperature() {
|
|
585
|
+
return this._temperature.min ? parseFloat(this._temperature.min) / 10 : null;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Gets the maximum supported temperature in Celsius
|
|
590
|
+
* @returns {number|null} Maximum temperature in Celsius or null if not available
|
|
591
|
+
*/
|
|
592
|
+
getMaxSupportedTemperature() {
|
|
593
|
+
return this._temperature.max ? parseFloat(this._temperature.max) / 10 : null;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Gets the lux (illuminance) reading.
|
|
599
|
+
*
|
|
600
|
+
* @returns {number|null} Lux value, or null if not available
|
|
601
|
+
* @example
|
|
602
|
+
* const lux = sensor.getLux();
|
|
603
|
+
* if (lux !== null) {
|
|
604
|
+
* console.log(`Light: ${lux} lx`);
|
|
605
|
+
* }
|
|
606
|
+
*/
|
|
607
|
+
getLux() {
|
|
608
|
+
return this._lux !== undefined && this._lux !== null ? this._lux : null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Hub Thermostat Valve subdevice (MTS100v3, etc.).
|
|
614
|
+
*
|
|
615
|
+
* This class represents smart thermostat valves that connect through a Meross hub.
|
|
616
|
+
* It provides comprehensive control over heating/cooling modes, target temperatures,
|
|
617
|
+
* presets, and valve on/off state.
|
|
618
|
+
*
|
|
619
|
+
* @class HubThermostatValve
|
|
620
|
+
* @extends MerossSubDevice
|
|
621
|
+
* @example
|
|
622
|
+
* const valve = hub.getSubdevice('valve123');
|
|
623
|
+
* if (valve instanceof HubThermostatValve) {
|
|
624
|
+
* // Turn valve on
|
|
625
|
+
* await valve.turnOn();
|
|
626
|
+
*
|
|
627
|
+
* // Set target temperature
|
|
628
|
+
* await valve.setTargetTemperature(22); // 22°C
|
|
629
|
+
*
|
|
630
|
+
* // Set thermostat mode
|
|
631
|
+
* await valve.setMode(ThermostatMode.AUTO);
|
|
632
|
+
*
|
|
633
|
+
* // Check if heating
|
|
634
|
+
* if (valve.isHeating()) {
|
|
635
|
+
* console.log('Valve is currently heating');
|
|
636
|
+
* }
|
|
637
|
+
*
|
|
638
|
+
* // Use preset temperatures
|
|
639
|
+
* await valve.setPresetTemperature('comfort', 23);
|
|
640
|
+
* const comfortTemp = valve.getPresetTemperature('comfort');
|
|
641
|
+
* }
|
|
642
|
+
*/
|
|
643
|
+
class HubThermostatValve extends MerossSubDevice {
|
|
644
|
+
/**
|
|
645
|
+
* Creates a new HubThermostatValve instance
|
|
646
|
+
* @param {string} hubDeviceUuid - UUID of the hub device
|
|
647
|
+
* @param {string} subdeviceId - Subdevice ID
|
|
648
|
+
* @param {Object} manager - The MerossCloud manager instance
|
|
649
|
+
* @param {Object} [kwargs] - Additional subdevice information
|
|
650
|
+
*/
|
|
651
|
+
constructor(hubDeviceUuid, subdeviceId, manager, kwargs = {}) {
|
|
652
|
+
super(hubDeviceUuid, subdeviceId, manager, kwargs);
|
|
653
|
+
|
|
654
|
+
this._togglex = {};
|
|
655
|
+
this._mode = {};
|
|
656
|
+
this._temperature = {};
|
|
657
|
+
this._adjust = {};
|
|
658
|
+
this._scheduleBMode = null;
|
|
659
|
+
this._lastActiveTime = null;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Namespace handler registry for routing notifications.
|
|
664
|
+
*
|
|
665
|
+
* Maps namespace strings to handler methods for processing incoming notifications.
|
|
666
|
+
*
|
|
667
|
+
* @private
|
|
668
|
+
*/
|
|
669
|
+
static get NAMESPACE_HANDLERS() {
|
|
670
|
+
return [
|
|
671
|
+
{
|
|
672
|
+
namespace: 'Appliance.Hub.Mts100.All',
|
|
673
|
+
handler: '_handleMts100All'
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
namespace: 'Appliance.Hub.ToggleX',
|
|
677
|
+
handler: '_handleToggleX'
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
namespace: 'Appliance.Hub.Mts100.Mode',
|
|
681
|
+
handler: '_handleMts100Mode'
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
namespace: 'Appliance.Hub.Mts100.Temperature',
|
|
685
|
+
handler: '_handleMts100Temperature'
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
namespace: 'Appliance.Hub.Online',
|
|
689
|
+
handler: '_handleOnline'
|
|
690
|
+
}
|
|
691
|
+
];
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Handles subdevice-specific notifications from the hub.
|
|
696
|
+
*
|
|
697
|
+
* Routes notifications to appropriate handler methods based on namespace registry.
|
|
698
|
+
*
|
|
699
|
+
* @param {string} namespace - Notification namespace
|
|
700
|
+
* @param {Object} data - Notification data payload
|
|
701
|
+
*/
|
|
702
|
+
async handleSubdeviceNotification(namespace, data) {
|
|
703
|
+
await super.handleSubdeviceNotification(namespace, data);
|
|
704
|
+
|
|
705
|
+
const handler = HubThermostatValve.NAMESPACE_HANDLERS.find(route => route.namespace === namespace);
|
|
706
|
+
if (handler && typeof this[handler.handler] === 'function') {
|
|
707
|
+
await this[handler.handler](data);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Handles Appliance.Hub.Mts100.All namespace.
|
|
713
|
+
*
|
|
714
|
+
* @private
|
|
715
|
+
* @param {Object} data - Notification data payload
|
|
716
|
+
*/
|
|
717
|
+
_handleMts100All(data) {
|
|
718
|
+
this._scheduleBMode = data.scheduleBMode;
|
|
719
|
+
|
|
720
|
+
const onlineStatus = this._updateOnlineStatus(data);
|
|
721
|
+
if (onlineStatus) {
|
|
722
|
+
this._onlineStatus = onlineStatus.status;
|
|
723
|
+
this._lastActiveTime = onlineStatus.lastActiveTime;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (data.togglex) {
|
|
727
|
+
this._togglex = { ...this._togglex, ...data.togglex };
|
|
728
|
+
}
|
|
729
|
+
if (data.mode) {
|
|
730
|
+
this._mode = { ...this._mode, ...data.mode };
|
|
731
|
+
}
|
|
732
|
+
if (data.temperature) {
|
|
733
|
+
this._temperature = { ...this._temperature, ...data.temperature };
|
|
734
|
+
this._temperature.latestSampleTime = Date.now();
|
|
735
|
+
}
|
|
736
|
+
if (data.adjust) {
|
|
737
|
+
this._adjust = { ...this._adjust, ...data.adjust };
|
|
738
|
+
this._adjust.latestSampleTime = Date.now();
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Handles Appliance.Hub.ToggleX namespace
|
|
744
|
+
* @private
|
|
745
|
+
* @param {Object} data - Notification data payload
|
|
746
|
+
*/
|
|
747
|
+
_handleToggleX(data) {
|
|
748
|
+
if (data.onoff !== undefined) {
|
|
749
|
+
this._togglex.onoff = data.onoff;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Handles Appliance.Hub.Mts100.Mode namespace
|
|
755
|
+
* @private
|
|
756
|
+
* @param {Object} data - Notification data payload
|
|
757
|
+
*/
|
|
758
|
+
_handleMts100Mode(data) {
|
|
759
|
+
if (data.state !== undefined) {
|
|
760
|
+
this._mode.state = data.state;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Handles Appliance.Hub.Mts100.Temperature namespace
|
|
766
|
+
* @private
|
|
767
|
+
* @param {Object} data - Notification data payload
|
|
768
|
+
*/
|
|
769
|
+
_handleMts100Temperature(data) {
|
|
770
|
+
if (data) {
|
|
771
|
+
this._temperature = { ...this._temperature, ...data };
|
|
772
|
+
this._temperature.latestSampleTime = Date.now();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Handles Appliance.Hub.Online namespace
|
|
778
|
+
* @private
|
|
779
|
+
* @param {Object} data - Notification data payload
|
|
780
|
+
*/
|
|
781
|
+
_handleOnline(data) {
|
|
782
|
+
const onlineStatus = this._updateOnlineStatus(data);
|
|
783
|
+
if (onlineStatus) {
|
|
784
|
+
this._onlineStatus = onlineStatus.status;
|
|
785
|
+
this._lastActiveTime = onlineStatus.lastActiveTime;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Checks if the valve is currently on.
|
|
791
|
+
*
|
|
792
|
+
* @returns {boolean} True if valve is on, false otherwise
|
|
793
|
+
* @example
|
|
794
|
+
* if (valve.isOn()) {
|
|
795
|
+
* console.log('Valve is open');
|
|
796
|
+
* } else {
|
|
797
|
+
* console.log('Valve is closed');
|
|
798
|
+
* }
|
|
799
|
+
*/
|
|
800
|
+
isOn() {
|
|
801
|
+
return this._togglex.onoff === 1;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Turns the valve on
|
|
806
|
+
* @returns {Promise<void>} Promise that resolves when command is sent
|
|
807
|
+
*/
|
|
808
|
+
async turnOn() {
|
|
809
|
+
await this.publishMessage('SET', 'Appliance.Hub.ToggleX', {
|
|
810
|
+
togglex: [{ id: this._subdeviceId, onoff: 1, channel: 0 }]
|
|
811
|
+
}, null);
|
|
812
|
+
this._togglex.onoff = 1;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Turns the valve off
|
|
817
|
+
* @returns {Promise<void>} Promise that resolves when command is sent
|
|
818
|
+
*/
|
|
819
|
+
async turnOff() {
|
|
820
|
+
await this.publishMessage('SET', 'Appliance.Hub.ToggleX', {
|
|
821
|
+
togglex: [{ id: this._subdeviceId, onoff: 0, channel: 0 }]
|
|
822
|
+
}, null);
|
|
823
|
+
this._togglex.onoff = 0;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Toggles the valve state (on/off)
|
|
828
|
+
* @returns {Promise<void>} Promise that resolves when command is sent
|
|
829
|
+
*/
|
|
830
|
+
async toggle() {
|
|
831
|
+
if (this.isOn()) {
|
|
832
|
+
await this.turnOff();
|
|
833
|
+
} else {
|
|
834
|
+
await this.turnOn();
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Gets the current thermostat mode
|
|
840
|
+
* @returns {number|undefined} Mode value from ThermostatMode enum or undefined if not available
|
|
841
|
+
*/
|
|
842
|
+
getMode() {
|
|
843
|
+
return this._mode.state;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Sets the thermostat mode.
|
|
848
|
+
*
|
|
849
|
+
* @param {number} mode - Mode value from {@link ThermostatMode} enum
|
|
850
|
+
* @returns {Promise<void>} Promise that resolves when command is sent
|
|
851
|
+
* @example
|
|
852
|
+
* // Set to auto mode
|
|
853
|
+
* await valve.setMode(ThermostatMode.AUTO);
|
|
854
|
+
*
|
|
855
|
+
* // Set to heating mode
|
|
856
|
+
* await valve.setMode(ThermostatMode.HEAT);
|
|
857
|
+
*/
|
|
858
|
+
async setMode(mode) {
|
|
859
|
+
await this.publishMessage('SET', 'Appliance.Hub.Mts100.Mode', {
|
|
860
|
+
mode: [{ id: this._subdeviceId, state: mode }]
|
|
861
|
+
}, null);
|
|
862
|
+
this._mode.state = mode;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Gets the target temperature in Celsius
|
|
867
|
+
* @returns {number|null} Target temperature in Celsius or null if not available
|
|
868
|
+
*/
|
|
869
|
+
getTargetTemperature() {
|
|
870
|
+
const temp = this._temperature.currentSet;
|
|
871
|
+
if (temp === undefined || temp === null) {
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
return parseFloat(temp) / 10.0;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Sets the target temperature
|
|
879
|
+
* @param {number} temperature - Target temperature in Celsius
|
|
880
|
+
* @returns {Promise<void>} Promise that resolves when command is sent
|
|
881
|
+
*/
|
|
882
|
+
async setTargetTemperature(temperature) {
|
|
883
|
+
const targetTemp = temperature * 10;
|
|
884
|
+
await this.publishMessage('SET', 'Appliance.Hub.Mts100.Temperature', {
|
|
885
|
+
temperature: [{ id: this._subdeviceId, custom: targetTemp }]
|
|
886
|
+
}, null);
|
|
887
|
+
this._temperature.currentSet = targetTemp;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Gets the last sampled room temperature in Celsius
|
|
892
|
+
* @returns {number|null} Room temperature in Celsius or null if not available
|
|
893
|
+
*/
|
|
894
|
+
getLastSampledTemperature() {
|
|
895
|
+
const temp = this._temperature.room;
|
|
896
|
+
if (temp === undefined || temp === null) {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
return parseFloat(temp) / 10.0;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Gets the minimum supported temperature in Celsius
|
|
904
|
+
* @returns {number|null} Minimum temperature in Celsius or null if not available
|
|
905
|
+
*/
|
|
906
|
+
getMinSupportedTemperature() {
|
|
907
|
+
const temp = this._temperature.min;
|
|
908
|
+
if (temp === undefined || temp === null) {
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
return parseFloat(temp) / 10.0;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Gets the maximum supported temperature in Celsius
|
|
916
|
+
* @returns {number|null} Maximum temperature in Celsius or null if not available
|
|
917
|
+
*/
|
|
918
|
+
getMaxSupportedTemperature() {
|
|
919
|
+
const temp = this._temperature.max;
|
|
920
|
+
if (temp === undefined || temp === null) {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
return parseFloat(temp) / 10.0;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Checks if the valve is currently heating
|
|
928
|
+
* @returns {boolean} True if heating, false otherwise
|
|
929
|
+
*/
|
|
930
|
+
isHeating() {
|
|
931
|
+
return this._temperature.heating === 1;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Checks if window open detection is active
|
|
936
|
+
* @returns {boolean} True if window is detected as open, false otherwise
|
|
937
|
+
*/
|
|
938
|
+
isWindowOpen() {
|
|
939
|
+
return this._temperature.openWindow === 1;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Gets the list of supported temperature presets
|
|
944
|
+
* @returns {Array<string>} Array of preset names: ['custom', 'comfort', 'economy', 'away']
|
|
945
|
+
*/
|
|
946
|
+
getSupportedPresets() {
|
|
947
|
+
return ['custom', 'comfort', 'economy', 'away'];
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Gets the temperature for a specific preset
|
|
952
|
+
* @param {string} preset - Preset name (must be one of: 'custom', 'comfort', 'economy', 'away')
|
|
953
|
+
* @returns {number|null} Temperature in Celsius or null if preset not supported or not available
|
|
954
|
+
*/
|
|
955
|
+
getPresetTemperature(preset) {
|
|
956
|
+
if (!this.getSupportedPresets().includes(preset)) {
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
959
|
+
const val = this._temperature[preset];
|
|
960
|
+
if (val === undefined || val === null) {
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
return parseFloat(val) / 10.0;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Sets the temperature for a specific preset
|
|
968
|
+
* @param {string} preset - Preset name (must be one of: 'custom', 'comfort', 'economy', 'away')
|
|
969
|
+
* @param {number} temperature - Temperature in Celsius
|
|
970
|
+
* @returns {Promise<void>} Promise that resolves when command is sent
|
|
971
|
+
* @throws {Error} If preset is not supported
|
|
972
|
+
*/
|
|
973
|
+
async setPresetTemperature(preset, temperature) {
|
|
974
|
+
if (!this.getSupportedPresets().includes(preset)) {
|
|
975
|
+
throw new CommandError(`Preset ${preset} is not supported`, { preset }, this.uuid);
|
|
976
|
+
}
|
|
977
|
+
const targetTemp = temperature * 10;
|
|
978
|
+
await this.publishMessage('SET', 'Appliance.Hub.Mts100.Temperature', {
|
|
979
|
+
temperature: [{ id: this._subdeviceId, [preset]: targetTemp }]
|
|
980
|
+
}, null);
|
|
981
|
+
this._temperature[preset] = targetTemp;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Gets the temperature adjustment offset in Celsius
|
|
986
|
+
* @returns {number|null} Adjustment offset in Celsius or null if not available
|
|
987
|
+
*/
|
|
988
|
+
getAdjust() {
|
|
989
|
+
const adjust = this._adjust.temperature;
|
|
990
|
+
if (adjust === undefined || adjust === null) {
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
return parseFloat(adjust) / 100.0;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Sets the temperature adjustment offset
|
|
998
|
+
* @param {number} temperature - Adjustment offset in Celsius
|
|
999
|
+
* @returns {Promise<void>} Promise that resolves when command is sent
|
|
1000
|
+
*/
|
|
1001
|
+
async setAdjust(temperature) {
|
|
1002
|
+
const adjustTemp = temperature * 100;
|
|
1003
|
+
await this.publishMessage('SET', 'Appliance.Hub.Mts100.Adjust', {
|
|
1004
|
+
adjust: [{ id: this._subdeviceId, temperature: adjustTemp }]
|
|
1005
|
+
}, null);
|
|
1006
|
+
this._adjust.temperature = adjustTemp;
|
|
1007
|
+
this._adjust.latestSampleTime = Date.now();
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Hub Water Leak Sensor subdevice (MS405, MS400, etc.).
|
|
1013
|
+
*
|
|
1014
|
+
* This class represents water leak detection sensors that connect through a Meross hub.
|
|
1015
|
+
* It provides methods to check for water leaks, access leak event history, and monitor
|
|
1016
|
+
* sensor status.
|
|
1017
|
+
*
|
|
1018
|
+
* @class HubWaterLeakSensor
|
|
1019
|
+
* @extends MerossSubDevice
|
|
1020
|
+
* @example
|
|
1021
|
+
* const sensor = hub.getSubdevice('leak123');
|
|
1022
|
+
* if (sensor instanceof HubWaterLeakSensor) {
|
|
1023
|
+
* // Check for leaks
|
|
1024
|
+
* const isLeaking = sensor.isLeaking();
|
|
1025
|
+
* if (isLeaking) {
|
|
1026
|
+
* console.warn('Water leak detected!');
|
|
1027
|
+
* const leakTime = sensor.getLatestDetectedWaterLeakTs();
|
|
1028
|
+
* console.log(`Leak detected at: ${new Date(leakTime * 1000)}`);
|
|
1029
|
+
* }
|
|
1030
|
+
*
|
|
1031
|
+
* // Get leak event history
|
|
1032
|
+
* const events = sensor.getLastEvents();
|
|
1033
|
+
* console.log(`Total events: ${events.length}`);
|
|
1034
|
+
* }
|
|
1035
|
+
*/
|
|
1036
|
+
class HubWaterLeakSensor extends MerossSubDevice {
|
|
1037
|
+
/**
|
|
1038
|
+
* Creates a new HubWaterLeakSensor instance
|
|
1039
|
+
* @param {string} hubDeviceUuid - UUID of the hub device
|
|
1040
|
+
* @param {string} subdeviceId - Subdevice ID
|
|
1041
|
+
* @param {Object} manager - The MerossCloud manager instance
|
|
1042
|
+
* @param {Object} [kwargs] - Additional subdevice information
|
|
1043
|
+
*/
|
|
1044
|
+
constructor(hubDeviceUuid, subdeviceId, manager, kwargs = {}) {
|
|
1045
|
+
super(hubDeviceUuid, subdeviceId, manager, kwargs);
|
|
1046
|
+
|
|
1047
|
+
this._waterLeakState = null;
|
|
1048
|
+
this._lastEventTs = null;
|
|
1049
|
+
this._cachedEvents = [];
|
|
1050
|
+
this._maxEventsQueueLen = 30;
|
|
1051
|
+
this._lastWaterLeakEventTs = null;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Namespace handler registry for routing notifications.
|
|
1056
|
+
*
|
|
1057
|
+
* Maps namespace strings to handler methods for processing incoming notifications.
|
|
1058
|
+
*
|
|
1059
|
+
* @private
|
|
1060
|
+
*/
|
|
1061
|
+
static get NAMESPACE_HANDLERS() {
|
|
1062
|
+
return [
|
|
1063
|
+
{
|
|
1064
|
+
namespace: 'Appliance.Hub.Sensor.WaterLeak',
|
|
1065
|
+
handler: '_handleWaterLeak'
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
namespace: 'Appliance.Hub.Sensor.All',
|
|
1069
|
+
handler: '_handleSensorAll'
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
namespace: 'Appliance.Hub.Online',
|
|
1073
|
+
handler: '_handleOnline'
|
|
1074
|
+
}
|
|
1075
|
+
];
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Handles subdevice-specific notifications from the hub.
|
|
1080
|
+
*
|
|
1081
|
+
* Routes notifications to appropriate handler methods based on namespace registry.
|
|
1082
|
+
*
|
|
1083
|
+
* @param {string} namespace - Notification namespace
|
|
1084
|
+
* @param {Object} data - Notification data payload
|
|
1085
|
+
*/
|
|
1086
|
+
async handleSubdeviceNotification(namespace, data) {
|
|
1087
|
+
await super.handleSubdeviceNotification(namespace, data);
|
|
1088
|
+
|
|
1089
|
+
const handler = HubWaterLeakSensor.NAMESPACE_HANDLERS.find(route => route.namespace === namespace);
|
|
1090
|
+
if (handler && typeof this[handler.handler] === 'function') {
|
|
1091
|
+
await this[handler.handler](data);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* Handles Appliance.Hub.Sensor.WaterLeak namespace
|
|
1097
|
+
* @private
|
|
1098
|
+
* @param {Object} data - Notification data payload
|
|
1099
|
+
*/
|
|
1100
|
+
_handleWaterLeak(data) {
|
|
1101
|
+
const { latestWaterLeak } = data;
|
|
1102
|
+
const { latestSampleTime } = data;
|
|
1103
|
+
|
|
1104
|
+
if (latestSampleTime !== undefined && latestSampleTime !== null) {
|
|
1105
|
+
this._handleWaterLeakFreshData(latestWaterLeak === 1, latestSampleTime);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Handles Appliance.Hub.Sensor.All namespace
|
|
1111
|
+
* @private
|
|
1112
|
+
* @param {Object} data - Notification data payload
|
|
1113
|
+
*/
|
|
1114
|
+
_handleSensorAll(data) {
|
|
1115
|
+
// Update online status if present
|
|
1116
|
+
const onlineStatus = this._updateOnlineStatus(data);
|
|
1117
|
+
if (onlineStatus) {
|
|
1118
|
+
this._onlineStatus = onlineStatus.status;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Extract water leak data if present
|
|
1122
|
+
if (data.waterLeak) {
|
|
1123
|
+
const { latestWaterLeak } = data.waterLeak;
|
|
1124
|
+
const { latestSampleTime } = data.waterLeak;
|
|
1125
|
+
|
|
1126
|
+
if (latestSampleTime !== undefined && latestSampleTime !== null) {
|
|
1127
|
+
this._handleWaterLeakFreshData(latestWaterLeak === 1, latestSampleTime);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Handles Appliance.Hub.Online namespace
|
|
1134
|
+
* @private
|
|
1135
|
+
* @param {Object} data - Notification data payload
|
|
1136
|
+
*/
|
|
1137
|
+
_handleOnline(data) {
|
|
1138
|
+
const onlineStatus = this._updateOnlineStatus(data);
|
|
1139
|
+
if (onlineStatus) {
|
|
1140
|
+
this._onlineStatus = onlineStatus.status;
|
|
1141
|
+
this._lastActiveTime = onlineStatus.lastActiveTime;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Checks if water leak is currently detected.
|
|
1147
|
+
*
|
|
1148
|
+
* @returns {boolean|null} True if leaking, false if not leaking, null if state is unknown
|
|
1149
|
+
* @example
|
|
1150
|
+
* const isLeaking = sensor.isLeaking();
|
|
1151
|
+
* if (isLeaking === true) {
|
|
1152
|
+
* console.warn('Water leak detected!');
|
|
1153
|
+
* } else if (isLeaking === false) {
|
|
1154
|
+
* console.log('No water leak detected');
|
|
1155
|
+
* } else {
|
|
1156
|
+
* console.log('Sensor state unknown');
|
|
1157
|
+
* }
|
|
1158
|
+
*/
|
|
1159
|
+
isLeaking() {
|
|
1160
|
+
return this._waterLeakState;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Gets the timestamp of the latest sample
|
|
1165
|
+
* @returns {number|null} Timestamp or null if no samples received
|
|
1166
|
+
*/
|
|
1167
|
+
getLatestSampleTime() {
|
|
1168
|
+
return this._lastEventTs;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Gets the timestamp of the latest detected water leak event
|
|
1173
|
+
* @returns {number|null} Timestamp or null if no leak events detected
|
|
1174
|
+
*/
|
|
1175
|
+
getLatestDetectedWaterLeakTs() {
|
|
1176
|
+
return this._lastWaterLeakEventTs;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Gets the last water leak events (cached queue, max 30 events)
|
|
1181
|
+
* @returns {Array<Object>} Array of event objects with leaking (boolean) and timestamp (number) properties
|
|
1182
|
+
*/
|
|
1183
|
+
getLastEvents() {
|
|
1184
|
+
return [...this._cachedEvents];
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* Handles fresh water leak data with timestamp validation.
|
|
1189
|
+
*
|
|
1190
|
+
* Ignores stale updates to maintain chronological order and prevent race conditions
|
|
1191
|
+
* from out-of-order notifications. Maintains bounded event history using FIFO queue.
|
|
1192
|
+
*
|
|
1193
|
+
* @private
|
|
1194
|
+
* @param {boolean} leaking - Whether water leak is detected
|
|
1195
|
+
* @param {number} timestamp - Event timestamp
|
|
1196
|
+
*/
|
|
1197
|
+
_handleWaterLeakFreshData(leaking, timestamp) {
|
|
1198
|
+
if (this._lastEventTs !== null && timestamp <= this._lastEventTs) {
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
if (this._lastEventTs === null || timestamp >= this._lastEventTs) {
|
|
1203
|
+
this._lastEventTs = timestamp;
|
|
1204
|
+
this._waterLeakState = leaking;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
if (leaking && (this._lastWaterLeakEventTs === null || timestamp >= this._lastWaterLeakEventTs)) {
|
|
1208
|
+
this._lastWaterLeakEventTs = timestamp;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
if (this._cachedEvents.length >= this._maxEventsQueueLen) {
|
|
1212
|
+
this._cachedEvents.shift();
|
|
1213
|
+
}
|
|
1214
|
+
this._cachedEvents.push({
|
|
1215
|
+
leaking,
|
|
1216
|
+
timestamp
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Hub Smoke Detector subdevice (MA151, etc.).
|
|
1223
|
+
*
|
|
1224
|
+
* This class represents smoke detector devices that connect through a Meross hub.
|
|
1225
|
+
* It provides methods to check alarm status, mute alarms, access test event history,
|
|
1226
|
+
* and monitor sensor status.
|
|
1227
|
+
*
|
|
1228
|
+
* @class HubSmokeDetector
|
|
1229
|
+
* @extends MerossSubDevice
|
|
1230
|
+
* @example
|
|
1231
|
+
* const detector = hub.getSubdevice('smoke123');
|
|
1232
|
+
* if (detector instanceof HubSmokeDetector) {
|
|
1233
|
+
* // Check alarm status
|
|
1234
|
+
* const status = detector.getSmokeAlarmStatus();
|
|
1235
|
+
* if (status === SmokeAlarmStatus.NORMAL) {
|
|
1236
|
+
* console.log('No alarms detected');
|
|
1237
|
+
* } else if (status === SmokeAlarmStatus.MUTE_SMOKE_ALARM) {
|
|
1238
|
+
* console.log('Smoke alarm is muted');
|
|
1239
|
+
* }
|
|
1240
|
+
*
|
|
1241
|
+
* // Mute smoke alarm
|
|
1242
|
+
* await detector.muteAlarm(true);
|
|
1243
|
+
*
|
|
1244
|
+
* // Get test events
|
|
1245
|
+
* const testEvents = detector.getTestEvents();
|
|
1246
|
+
* console.log(`Test events: ${testEvents.length}`);
|
|
1247
|
+
*
|
|
1248
|
+
* // Refresh alarm status
|
|
1249
|
+
* await detector.refreshAlarmStatus();
|
|
1250
|
+
* }
|
|
1251
|
+
*/
|
|
1252
|
+
class HubSmokeDetector extends MerossSubDevice {
|
|
1253
|
+
/**
|
|
1254
|
+
* Creates a new HubSmokeDetector instance
|
|
1255
|
+
* @param {string} hubDeviceUuid - UUID of the hub device
|
|
1256
|
+
* @param {string} subdeviceId - Subdevice ID
|
|
1257
|
+
* @param {Object} manager - The MerossCloud manager instance
|
|
1258
|
+
* @param {Object} [kwargs] - Additional subdevice information
|
|
1259
|
+
*/
|
|
1260
|
+
constructor(hubDeviceUuid, subdeviceId, manager, kwargs = {}) {
|
|
1261
|
+
super(hubDeviceUuid, subdeviceId, manager, kwargs);
|
|
1262
|
+
|
|
1263
|
+
this._alarmStatus = null;
|
|
1264
|
+
this._interConn = null;
|
|
1265
|
+
this._lastStatusUpdate = null;
|
|
1266
|
+
this._testEvents = [];
|
|
1267
|
+
this._maxTestEvents = 10;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* Namespace handler registry for routing notifications.
|
|
1272
|
+
*
|
|
1273
|
+
* Maps namespace strings to handler methods for processing incoming notifications.
|
|
1274
|
+
*
|
|
1275
|
+
* @private
|
|
1276
|
+
*/
|
|
1277
|
+
static get NAMESPACE_HANDLERS() {
|
|
1278
|
+
return [
|
|
1279
|
+
{
|
|
1280
|
+
namespace: 'Appliance.Hub.Sensor.Smoke',
|
|
1281
|
+
handler: '_handleSmoke'
|
|
1282
|
+
},
|
|
1283
|
+
{
|
|
1284
|
+
namespace: 'Appliance.Hub.Sensor.All',
|
|
1285
|
+
handler: '_handleSensorAll'
|
|
1286
|
+
},
|
|
1287
|
+
{
|
|
1288
|
+
namespace: 'Appliance.Hub.Online',
|
|
1289
|
+
handler: '_handleOnline'
|
|
1290
|
+
}
|
|
1291
|
+
];
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* Handles subdevice-specific notifications from the hub.
|
|
1296
|
+
*
|
|
1297
|
+
* Routes notifications to appropriate handler methods based on namespace registry.
|
|
1298
|
+
*
|
|
1299
|
+
* @param {string} namespace - Notification namespace
|
|
1300
|
+
* @param {Object} data - Notification data payload
|
|
1301
|
+
*/
|
|
1302
|
+
async handleSubdeviceNotification(namespace, data) {
|
|
1303
|
+
await super.handleSubdeviceNotification(namespace, data);
|
|
1304
|
+
|
|
1305
|
+
const handler = HubSmokeDetector.NAMESPACE_HANDLERS.find(route => route.namespace === namespace);
|
|
1306
|
+
if (handler && typeof this[handler.handler] === 'function') {
|
|
1307
|
+
await this[handler.handler](data);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* Handles Appliance.Hub.Sensor.Smoke namespace
|
|
1313
|
+
* @private
|
|
1314
|
+
* @param {Object} data - Notification data payload
|
|
1315
|
+
*/
|
|
1316
|
+
_handleSmoke(data) {
|
|
1317
|
+
this._handleSmokeAlarmData(data);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
/**
|
|
1321
|
+
* Handles Appliance.Hub.Sensor.All namespace
|
|
1322
|
+
* @private
|
|
1323
|
+
* @param {Object} data - Notification data payload
|
|
1324
|
+
*/
|
|
1325
|
+
_handleSensorAll(data) {
|
|
1326
|
+
// Update online status if present
|
|
1327
|
+
const onlineStatus = this._updateOnlineStatus(data);
|
|
1328
|
+
if (onlineStatus) {
|
|
1329
|
+
this._onlineStatus = onlineStatus.status;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// Extract smoke alarm data if present
|
|
1333
|
+
if (data.smokeAlarm) {
|
|
1334
|
+
this._handleSmokeAlarmData(data.smokeAlarm);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/**
|
|
1339
|
+
* Handles Appliance.Hub.Online namespace
|
|
1340
|
+
* @private
|
|
1341
|
+
* @param {Object} data - Notification data payload
|
|
1342
|
+
*/
|
|
1343
|
+
_handleOnline(data) {
|
|
1344
|
+
const onlineStatus = this._updateOnlineStatus(data);
|
|
1345
|
+
if (onlineStatus) {
|
|
1346
|
+
this._onlineStatus = onlineStatus.status;
|
|
1347
|
+
this._lastActiveTime = onlineStatus.lastActiveTime;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/**
|
|
1352
|
+
* Handles smoke alarm data with timestamp validation.
|
|
1353
|
+
*
|
|
1354
|
+
* Normalizes array format responses to single object and validates timestamp freshness
|
|
1355
|
+
* to prevent state regression from out-of-order notifications.
|
|
1356
|
+
*
|
|
1357
|
+
* @private
|
|
1358
|
+
* @param {Object|Array} data - Smoke alarm data (can be object or array)
|
|
1359
|
+
*/
|
|
1360
|
+
_handleSmokeAlarmData(data) {
|
|
1361
|
+
const alarmData = this._normalizeAlarmData(data);
|
|
1362
|
+
|
|
1363
|
+
const status = alarmData?.status;
|
|
1364
|
+
const interConn = alarmData?.interConn;
|
|
1365
|
+
const timestamp = alarmData?.timestamp;
|
|
1366
|
+
const event = alarmData?.event;
|
|
1367
|
+
|
|
1368
|
+
if (!this._validateAlarmTimestamp(timestamp)) {
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
if (timestamp !== undefined && timestamp !== null) {
|
|
1373
|
+
this._lastStatusUpdate = timestamp;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
if (status !== undefined && status !== null) {
|
|
1377
|
+
this._alarmStatus = status;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
if (interConn !== undefined && interConn !== null) {
|
|
1381
|
+
this._interConn = interConn;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
if (event && event.test) {
|
|
1385
|
+
this._processTestEvent(event.test);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Normalizes alarm data from array format to single object.
|
|
1391
|
+
*
|
|
1392
|
+
* @private
|
|
1393
|
+
* @param {Object|Array} data - Smoke alarm data (can be object or array)
|
|
1394
|
+
* @returns {Object} Normalized alarm data object
|
|
1395
|
+
*/
|
|
1396
|
+
_normalizeAlarmData(data) {
|
|
1397
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
1398
|
+
return data[0];
|
|
1399
|
+
}
|
|
1400
|
+
return data;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/**
|
|
1404
|
+
* Validates alarm timestamp freshness.
|
|
1405
|
+
*
|
|
1406
|
+
* @private
|
|
1407
|
+
* @param {number|undefined|null} timestamp - Timestamp to validate
|
|
1408
|
+
* @returns {boolean} True if update should proceed, false if stale
|
|
1409
|
+
*/
|
|
1410
|
+
_validateAlarmTimestamp(timestamp) {
|
|
1411
|
+
if (timestamp === undefined || timestamp === null) {
|
|
1412
|
+
return true;
|
|
1413
|
+
}
|
|
1414
|
+
if (this._lastStatusUpdate !== null && timestamp <= this._lastStatusUpdate) {
|
|
1415
|
+
return false;
|
|
1416
|
+
}
|
|
1417
|
+
return true;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Processes test event data.
|
|
1422
|
+
*
|
|
1423
|
+
* Maintains FIFO queue with size limit to prevent unbounded memory growth.
|
|
1424
|
+
*
|
|
1425
|
+
* @private
|
|
1426
|
+
* @param {Object} testEvent - Test event object with type and timestamp
|
|
1427
|
+
*/
|
|
1428
|
+
_processTestEvent(testEvent) {
|
|
1429
|
+
const testType = testEvent.type;
|
|
1430
|
+
const testTimestamp = testEvent.timestamp;
|
|
1431
|
+
|
|
1432
|
+
if (this._testEvents.length >= this._maxTestEvents) {
|
|
1433
|
+
this._testEvents.shift();
|
|
1434
|
+
}
|
|
1435
|
+
this._testEvents.push({
|
|
1436
|
+
type: testType,
|
|
1437
|
+
timestamp: testTimestamp
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/**
|
|
1442
|
+
* Gets the current smoke alarm status.
|
|
1443
|
+
*
|
|
1444
|
+
* @returns {number|null} Alarm status code from {@link SmokeAlarmStatus} enum (23, 26, 27), or null if unknown
|
|
1445
|
+
* @example
|
|
1446
|
+
* const status = detector.getSmokeAlarmStatus();
|
|
1447
|
+
* if (status === SmokeAlarmStatus.NORMAL) {
|
|
1448
|
+
* console.log('No alarms');
|
|
1449
|
+
* } else if (status === SmokeAlarmStatus.MUTE_SMOKE_ALARM) {
|
|
1450
|
+
* console.log('Smoke alarm muted');
|
|
1451
|
+
* }
|
|
1452
|
+
*/
|
|
1453
|
+
getSmokeAlarmStatus() {
|
|
1454
|
+
return this._alarmStatus;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Gets the interconnection status
|
|
1459
|
+
* @returns {number|null} Interconnection status (0 = not interconnected, 1 = interconnected) or null if unknown
|
|
1460
|
+
*/
|
|
1461
|
+
getInterConnStatus() {
|
|
1462
|
+
return this._interConn;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/**
|
|
1466
|
+
* Gets the timestamp of the last status update
|
|
1467
|
+
* @returns {number|null} Timestamp or null if no updates received
|
|
1468
|
+
*/
|
|
1469
|
+
getLastStatusUpdate() {
|
|
1470
|
+
return this._lastStatusUpdate;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/**
|
|
1474
|
+
* Mutes the smoke alarm or temperature alarm.
|
|
1475
|
+
*
|
|
1476
|
+
* @param {boolean} [muteSmoke=true] - If true, mute smoke alarm (status 27), else mute temperature alarm (status 26)
|
|
1477
|
+
* @returns {Promise<Object>} Promise that resolves with the response from the device
|
|
1478
|
+
* @example
|
|
1479
|
+
* // Mute smoke alarm
|
|
1480
|
+
* await detector.muteAlarm(true);
|
|
1481
|
+
*
|
|
1482
|
+
* // Mute temperature alarm
|
|
1483
|
+
* await detector.muteAlarm(false);
|
|
1484
|
+
*/
|
|
1485
|
+
async muteAlarm(muteSmoke = true) {
|
|
1486
|
+
const status = muteSmoke ? SmokeAlarmStatus.MUTE_SMOKE_ALARM : SmokeAlarmStatus.MUTE_TEMPERATURE_ALARM;
|
|
1487
|
+
|
|
1488
|
+
const response = await this.publishMessage('SET', 'Appliance.Hub.Sensor.Smoke', {
|
|
1489
|
+
smokeAlarm: [{
|
|
1490
|
+
id: this._subdeviceId,
|
|
1491
|
+
status
|
|
1492
|
+
}]
|
|
1493
|
+
}, null);
|
|
1494
|
+
|
|
1495
|
+
if (response) {
|
|
1496
|
+
this._alarmStatus = status;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
return response;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/**
|
|
1503
|
+
* Gets the cached test events
|
|
1504
|
+
* @returns {Array<Object>} Array of test event objects with type and timestamp properties
|
|
1505
|
+
*/
|
|
1506
|
+
getTestEvents() {
|
|
1507
|
+
return [...this._testEvents];
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* Refreshes the smoke alarm status from the device.
|
|
1512
|
+
*
|
|
1513
|
+
* @returns {Promise<Object>} Promise that resolves with the alarm status response
|
|
1514
|
+
*/
|
|
1515
|
+
async refreshAlarmStatus() {
|
|
1516
|
+
const response = await this.publishMessage('GET', 'Appliance.Hub.Sensor.Smoke', {
|
|
1517
|
+
smokeAlarm: [{
|
|
1518
|
+
id: this._subdeviceId
|
|
1519
|
+
}]
|
|
1520
|
+
}, null);
|
|
1521
|
+
|
|
1522
|
+
if (response && response.smokeAlarm) {
|
|
1523
|
+
this._handleSmokeAlarmData(response.smokeAlarm);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
return response;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
module.exports = {
|
|
1531
|
+
MerossSubDevice,
|
|
1532
|
+
HubTempHumSensor,
|
|
1533
|
+
HubThermostatValve,
|
|
1534
|
+
HubWaterLeakSensor,
|
|
1535
|
+
HubSmokeDetector
|
|
1536
|
+
};
|
|
1537
|
+
|