meross-iot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +153 -0
  4. package/index.d.ts +2344 -0
  5. package/index.js +131 -0
  6. package/lib/controller/device.js +1317 -0
  7. package/lib/controller/features/alarm-feature.js +89 -0
  8. package/lib/controller/features/child-lock-feature.js +61 -0
  9. package/lib/controller/features/config-feature.js +54 -0
  10. package/lib/controller/features/consumption-feature.js +210 -0
  11. package/lib/controller/features/control-feature.js +62 -0
  12. package/lib/controller/features/diffuser-feature.js +411 -0
  13. package/lib/controller/features/digest-timer-feature.js +22 -0
  14. package/lib/controller/features/digest-trigger-feature.js +22 -0
  15. package/lib/controller/features/dnd-feature.js +79 -0
  16. package/lib/controller/features/electricity-feature.js +144 -0
  17. package/lib/controller/features/encryption-feature.js +259 -0
  18. package/lib/controller/features/garage-feature.js +337 -0
  19. package/lib/controller/features/hub-feature.js +687 -0
  20. package/lib/controller/features/light-feature.js +408 -0
  21. package/lib/controller/features/presence-sensor-feature.js +297 -0
  22. package/lib/controller/features/roller-shutter-feature.js +456 -0
  23. package/lib/controller/features/runtime-feature.js +74 -0
  24. package/lib/controller/features/screen-feature.js +67 -0
  25. package/lib/controller/features/sensor-history-feature.js +47 -0
  26. package/lib/controller/features/smoke-config-feature.js +50 -0
  27. package/lib/controller/features/spray-feature.js +166 -0
  28. package/lib/controller/features/system-feature.js +269 -0
  29. package/lib/controller/features/temp-unit-feature.js +55 -0
  30. package/lib/controller/features/thermostat-feature.js +804 -0
  31. package/lib/controller/features/timer-feature.js +507 -0
  32. package/lib/controller/features/toggle-feature.js +223 -0
  33. package/lib/controller/features/trigger-feature.js +333 -0
  34. package/lib/controller/hub-device.js +185 -0
  35. package/lib/controller/subdevice.js +1537 -0
  36. package/lib/device-factory.js +463 -0
  37. package/lib/error-budget.js +138 -0
  38. package/lib/http-api.js +766 -0
  39. package/lib/manager.js +1609 -0
  40. package/lib/model/channel-info.js +79 -0
  41. package/lib/model/constants.js +119 -0
  42. package/lib/model/enums.js +819 -0
  43. package/lib/model/exception.js +363 -0
  44. package/lib/model/http/device.js +215 -0
  45. package/lib/model/http/error-codes.js +121 -0
  46. package/lib/model/http/exception.js +151 -0
  47. package/lib/model/http/subdevice.js +133 -0
  48. package/lib/model/push/alarm.js +112 -0
  49. package/lib/model/push/bind.js +97 -0
  50. package/lib/model/push/common.js +282 -0
  51. package/lib/model/push/diffuser-light.js +100 -0
  52. package/lib/model/push/diffuser-spray.js +83 -0
  53. package/lib/model/push/factory.js +229 -0
  54. package/lib/model/push/generic.js +115 -0
  55. package/lib/model/push/hub-battery.js +59 -0
  56. package/lib/model/push/hub-mts100-all.js +64 -0
  57. package/lib/model/push/hub-mts100-mode.js +59 -0
  58. package/lib/model/push/hub-mts100-temperature.js +62 -0
  59. package/lib/model/push/hub-online.js +59 -0
  60. package/lib/model/push/hub-sensor-alert.js +61 -0
  61. package/lib/model/push/hub-sensor-all.js +59 -0
  62. package/lib/model/push/hub-sensor-smoke.js +110 -0
  63. package/lib/model/push/hub-sensor-temphum.js +62 -0
  64. package/lib/model/push/hub-subdevicelist.js +50 -0
  65. package/lib/model/push/hub-togglex.js +60 -0
  66. package/lib/model/push/index.js +81 -0
  67. package/lib/model/push/online.js +53 -0
  68. package/lib/model/push/presence-study.js +61 -0
  69. package/lib/model/push/sensor-latestx.js +106 -0
  70. package/lib/model/push/timerx.js +63 -0
  71. package/lib/model/push/togglex.js +78 -0
  72. package/lib/model/push/triggerx.js +62 -0
  73. package/lib/model/push/unbind.js +34 -0
  74. package/lib/model/push/water-leak.js +107 -0
  75. package/lib/model/states/diffuser-light-state.js +119 -0
  76. package/lib/model/states/diffuser-spray-state.js +58 -0
  77. package/lib/model/states/garage-door-state.js +71 -0
  78. package/lib/model/states/index.js +38 -0
  79. package/lib/model/states/light-state.js +134 -0
  80. package/lib/model/states/presence-sensor-state.js +239 -0
  81. package/lib/model/states/roller-shutter-state.js +82 -0
  82. package/lib/model/states/spray-state.js +58 -0
  83. package/lib/model/states/thermostat-state.js +297 -0
  84. package/lib/model/states/timer-state.js +192 -0
  85. package/lib/model/states/toggle-state.js +105 -0
  86. package/lib/model/states/trigger-state.js +155 -0
  87. package/lib/subscription.js +587 -0
  88. package/lib/utilities/conversion.js +62 -0
  89. package/lib/utilities/debug.js +165 -0
  90. package/lib/utilities/mqtt.js +152 -0
  91. package/lib/utilities/network.js +53 -0
  92. package/lib/utilities/options.js +64 -0
  93. package/lib/utilities/request-queue.js +161 -0
  94. package/lib/utilities/ssid.js +37 -0
  95. package/lib/utilities/state-changes.js +66 -0
  96. package/lib/utilities/stats.js +687 -0
  97. package/lib/utilities/timer.js +310 -0
  98. package/lib/utilities/trigger.js +286 -0
  99. package/package.json +73 -0
@@ -0,0 +1,463 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Maps Meross API namespace strings to device feature modules.
5
+ *
6
+ * Used during device class construction to dynamically compose device classes
7
+ * based on the capabilities reported by each device. This avoids maintaining
8
+ * separate classes for every device type and version combination.
9
+ *
10
+ * @private
11
+ */
12
+ const ABILITY_MATRIX = {
13
+ // Power plugs abilities
14
+ 'Appliance.Control.ToggleX': require('./controller/features/toggle-feature'),
15
+ 'Appliance.Control.Toggle': require('./controller/features/toggle-feature'),
16
+ 'Appliance.Control.ConsumptionX': require('./controller/features/consumption-feature'),
17
+ 'Appliance.Control.Consumption': require('./controller/features/consumption-feature'),
18
+ 'Appliance.Control.Electricity': require('./controller/features/electricity-feature'),
19
+ 'Appliance.Control.Alarm': require('./controller/features/alarm-feature'),
20
+
21
+ // Timer and Trigger
22
+ 'Appliance.Control.TimerX': require('./controller/features/timer-feature'),
23
+ 'Appliance.Digest.TimerX': require('./controller/features/digest-timer-feature'),
24
+ 'Appliance.Control.TriggerX': require('./controller/features/trigger-feature'),
25
+ 'Appliance.Digest.TriggerX': require('./controller/features/digest-trigger-feature'),
26
+
27
+ // Light abilities
28
+ 'Appliance.Control.Light': require('./controller/features/light-feature'),
29
+
30
+ // Garage opener
31
+ 'Appliance.GarageDoor.State': require('./controller/features/garage-feature'),
32
+
33
+ // Roller shutter
34
+ 'Appliance.RollerShutter.State': require('./controller/features/roller-shutter-feature'),
35
+
36
+ // Spray/Humidifier
37
+ 'Appliance.Control.Spray': require('./controller/features/spray-feature'),
38
+
39
+ // Diffuser
40
+ 'Appliance.Control.Diffuser.Light': require('./controller/features/diffuser-feature'),
41
+ 'Appliance.Control.Diffuser.Spray': require('./controller/features/diffuser-feature'),
42
+
43
+ // Thermostat
44
+ 'Appliance.Control.Thermostat.Mode': require('./controller/features/thermostat-feature'),
45
+ 'Appliance.Control.Thermostat.ModeB': require('./controller/features/thermostat-feature'),
46
+
47
+ // System (always included via buildDevice, but listed here for completeness)
48
+ 'Appliance.System.All': require('./controller/features/system-feature'),
49
+ 'Appliance.System.Online': require('./controller/features/system-feature'),
50
+ 'Appliance.System.Hardware': require('./controller/features/system-feature'),
51
+ 'Appliance.System.Firmware': require('./controller/features/system-feature'),
52
+ 'Appliance.System.Time': require('./controller/features/system-feature'),
53
+ 'Appliance.System.Clock': require('./controller/features/system-feature'),
54
+ 'Appliance.System.Position': require('./controller/features/system-feature'),
55
+ 'Appliance.System.Ability': require('./controller/features/system-feature'),
56
+ 'Appliance.System.Report': require('./controller/features/system-feature'),
57
+ 'Appliance.System.Debug': require('./controller/features/system-feature'),
58
+ 'Appliance.System.Factory': require('./controller/features/system-feature'),
59
+ 'Appliance.System.LedMode': require('./controller/features/system-feature'),
60
+ 'Appliance.Mcu.Firmware': require('./controller/features/system-feature'),
61
+
62
+ // Encryption
63
+ 'Appliance.Encrypt.Suite': require('./controller/features/encryption-feature'),
64
+ 'Appliance.Encrypt.ECDHE': require('./controller/features/encryption-feature'),
65
+
66
+ // DND
67
+ 'Appliance.System.DNDMode': require('./controller/features/dnd-feature'),
68
+
69
+ // Runtime
70
+ 'Appliance.System.Runtime': require('./controller/features/runtime-feature'),
71
+
72
+ // Hub functionality (all hub features combined in single file)
73
+ 'Appliance.Hub.Online': require('./controller/features/hub-feature'),
74
+ 'Appliance.Hub.ToggleX': require('./controller/features/hub-feature'),
75
+ 'Appliance.Hub.Battery': require('./controller/features/hub-feature'),
76
+ 'Appliance.Hub.Sensor.WaterLeak': require('./controller/features/hub-feature'),
77
+ 'Appliance.Hub.Sensor.All': require('./controller/features/hub-feature'),
78
+ 'Appliance.Hub.Sensor.TempHum': require('./controller/features/hub-feature'),
79
+ 'Appliance.Hub.Sensor.Alert': require('./controller/features/hub-feature'),
80
+ 'Appliance.Hub.Sensor.Smoke': require('./controller/features/hub-feature'),
81
+ 'Appliance.Hub.Sensor.Adjust': require('./controller/features/hub-feature'),
82
+ 'Appliance.Hub.Sensor.DoorWindow': require('./controller/features/hub-feature'),
83
+ 'Appliance.Hub.Mts100.All': require('./controller/features/hub-feature'),
84
+ 'Appliance.Hub.Mts100.Mode': require('./controller/features/hub-feature'),
85
+ 'Appliance.Hub.Mts100.Temperature': require('./controller/features/hub-feature'),
86
+ 'Appliance.Hub.Mts100.Adjust': require('./controller/features/hub-feature'),
87
+ 'Appliance.Hub.Mts100.SuperCtl': require('./controller/features/hub-feature'),
88
+ 'Appliance.Hub.Mts100.ScheduleB': require('./controller/features/hub-feature'),
89
+ 'Appliance.Hub.Mts100.Config': require('./controller/features/hub-feature'),
90
+ 'Appliance.Hub.Exception': require('./controller/features/hub-feature'),
91
+ 'Appliance.Hub.Report': require('./controller/features/hub-feature'),
92
+ 'Appliance.Hub.PairSubDev': require('./controller/features/hub-feature'),
93
+ 'Appliance.Hub.SubDevice.Beep': require('./controller/features/hub-feature'),
94
+ 'Appliance.Hub.SubDevice.MotorAdjust': require('./controller/features/hub-feature'),
95
+ 'Appliance.Hub.SubDevice.Version': require('./controller/features/hub-feature'),
96
+
97
+ // Roller Shutter
98
+ 'Appliance.RollerShutter.Position': require('./controller/features/roller-shutter-feature'),
99
+ 'Appliance.RollerShutter.Config': require('./controller/features/roller-shutter-feature'),
100
+ 'Appliance.RollerShutter.Adjust': require('./controller/features/roller-shutter-feature'),
101
+
102
+ // Thermostat additional namespaces
103
+ 'Appliance.Control.Thermostat.Schedule': require('./controller/features/thermostat-feature'),
104
+ 'Appliance.Control.Thermostat.Timer': require('./controller/features/thermostat-feature'),
105
+ 'Appliance.Control.Thermostat.Alarm': require('./controller/features/thermostat-feature'),
106
+ 'Appliance.Control.Thermostat.WindowOpened': require('./controller/features/thermostat-feature'),
107
+ 'Appliance.Control.Thermostat.HoldAction': require('./controller/features/thermostat-feature'),
108
+ 'Appliance.Control.Thermostat.Overheat': require('./controller/features/thermostat-feature'),
109
+ 'Appliance.Control.Thermostat.DeadZone': require('./controller/features/thermostat-feature'),
110
+ 'Appliance.Control.Thermostat.Calibration': require('./controller/features/thermostat-feature'),
111
+ 'Appliance.Control.Thermostat.Sensor': require('./controller/features/thermostat-feature'),
112
+ 'Appliance.Control.Thermostat.SummerMode': require('./controller/features/thermostat-feature'),
113
+ 'Appliance.Control.Thermostat.Frost': require('./controller/features/thermostat-feature'),
114
+ 'Appliance.Control.Thermostat.AlarmConfig': require('./controller/features/thermostat-feature'),
115
+ 'Appliance.Control.Thermostat.CompressorDelay': require('./controller/features/thermostat-feature'),
116
+ 'Appliance.Control.Thermostat.CtlRange': require('./controller/features/thermostat-feature'),
117
+
118
+ // Config namespaces
119
+ 'Appliance.Config.OverTemp': require('./controller/features/config-feature'),
120
+
121
+ // Control namespaces
122
+ 'Appliance.Control.Multiple': require('./controller/features/control-feature'),
123
+ 'Appliance.Control.Upgrade': require('./controller/features/control-feature'),
124
+ 'Appliance.Control.OverTemp': require('./controller/features/control-feature'),
125
+ 'Appliance.Control.ConsumptionConfig': require('./controller/features/consumption-feature'),
126
+ 'Appliance.Control.Diffuser.Sensor': require('./controller/features/diffuser-feature'),
127
+ 'Appliance.Control.PhysicalLock': require('./controller/features/child-lock-feature'),
128
+ 'Appliance.Control.Screen.Brightness': require('./controller/features/screen-feature'),
129
+ 'Appliance.Control.Sensor.History': require('./controller/features/sensor-history-feature'),
130
+ 'Appliance.Control.Sensor.LatestX': require('./controller/features/presence-sensor-feature'),
131
+ 'Appliance.Control.Smoke.Config': require('./controller/features/smoke-config-feature'),
132
+ 'Appliance.Control.TempUnit': require('./controller/features/temp-unit-feature'),
133
+
134
+ // Presence sensor
135
+ 'Appliance.Control.Presence.Config': require('./controller/features/presence-sensor-feature'),
136
+ 'Appliance.Control.Presence.Study': require('./controller/features/presence-sensor-feature'),
137
+
138
+ // Garage door additional namespaces
139
+ 'Appliance.GarageDoor.MultipleConfig': require('./controller/features/garage-feature'),
140
+ 'Appliance.GarageDoor.Config': require('./controller/features/garage-feature')
141
+ };
142
+
143
+ /**
144
+ * Namespace that identifies hub devices.
145
+ *
146
+ * Used to distinguish hub devices from regular devices during device creation,
147
+ * since hubs require a different base class and constructor signature.
148
+ *
149
+ * @private
150
+ */
151
+ const HUB_DISCRIMINATING_ABILITY = 'Appliance.Hub.SubdeviceList';
152
+
153
+ /**
154
+ * Cache for dynamically created device classes.
155
+ *
156
+ * Caching avoids recreating identical classes for devices with the same type and versions,
157
+ * reducing memory usage and improving instantiation performance.
158
+ *
159
+ * @type {Map<string, Function>}
160
+ * Key: type string (e.g., "mss310:1.0.0:4.2.1")
161
+ * Value: Device class constructor
162
+ */
163
+ const _dynamicTypes = new Map();
164
+
165
+ /**
166
+ * Generates a cache key from device type and version information.
167
+ *
168
+ * The key format `deviceType:hardwareVersion:firmwareVersion` ensures devices with
169
+ * different capabilities (due to version differences) get separate cached classes.
170
+ *
171
+ * @param {string} deviceType - Device type (e.g., "mss310", "msl120")
172
+ * @param {string} [hardwareVersion] - Hardware version (e.g., "1.0.0"). Defaults to 'unknown' if not provided.
173
+ * @param {string} [firmwareVersion] - Firmware version (e.g., "4.2.1"). Defaults to 'unknown' if not provided.
174
+ * @returns {string} Cache key string (e.g., "mss310:1.0.0:4.2.1")
175
+ */
176
+ function getTypeKey(deviceType, hardwareVersion, firmwareVersion) {
177
+ const hw = hardwareVersion || 'unknown';
178
+ const fw = firmwareVersion || 'unknown';
179
+ return `${deviceType}:${hw}:${fw}`;
180
+ }
181
+
182
+ /**
183
+ * Retrieves a cached device class if one exists for the given type and versions.
184
+ *
185
+ * Device classes are cached to avoid recreating identical classes for devices
186
+ * with the same capabilities, reducing memory usage and improving performance.
187
+ *
188
+ * @param {string} deviceType - Device type (e.g., "mss310")
189
+ * @param {string} [hardwareVersion] - Hardware version (e.g., "1.0.0")
190
+ * @param {string} [firmwareVersion] - Firmware version (e.g., "4.2.1")
191
+ * @returns {Function|null} Cached device class constructor, or null if not found in cache
192
+ */
193
+ function getCachedDeviceClass(deviceType, hardwareVersion, firmwareVersion) {
194
+ const typeKey = getTypeKey(deviceType, hardwareVersion, firmwareVersion);
195
+ return _dynamicTypes.get(typeKey) || null;
196
+ }
197
+
198
+ /**
199
+ * Builds a device class dynamically by composing features based on device abilities.
200
+ *
201
+ * Features are selected from the ability matrix and applied to a base class. Handlers
202
+ * are chained so multiple features can process the same events or state updates.
203
+ *
204
+ * @param {string} typeKey - Type key for caching
205
+ * @param {Object} abilities - Device abilities dictionary
206
+ * @param {Function} BaseClass - Base class (MerossDevice or MerossHubDevice)
207
+ * @returns {Function} Dynamic device class
208
+ */
209
+ function _buildDynamicClass(typeKey, abilities, BaseClass) {
210
+ const features = new Set();
211
+
212
+ // System feature provides core device functionality required by all devices
213
+ features.add(require('./controller/features/system-feature'));
214
+
215
+ if (abilities && typeof abilities === 'object') {
216
+ // X-suffixed namespaces are extended versions that replace base namespaces
217
+ // Track base namespaces to avoid adding both base and X versions
218
+ const hasXVersion = new Set();
219
+ for (const namespace of Object.keys(abilities)) {
220
+ if (namespace.endsWith('X')) {
221
+ const baseNamespace = namespace.slice(0, -1);
222
+ hasXVersion.add(baseNamespace);
223
+ }
224
+ }
225
+
226
+ // Prefer X versions over base versions to use the most capable feature implementation
227
+ for (const [namespace] of Object.entries(abilities)) {
228
+ const feature = ABILITY_MATRIX[namespace];
229
+ if (feature) {
230
+ if (namespace.endsWith('X')) {
231
+ features.add(feature);
232
+ } else {
233
+ // Only add base version if no X version exists
234
+ if (!hasXVersion.has(namespace)) {
235
+ features.add(feature);
236
+ }
237
+ }
238
+ }
239
+ }
240
+ }
241
+
242
+ class DynamicDevice extends BaseClass {}
243
+
244
+ const featureArray = Array.from(features);
245
+ const pushHandlers = [];
246
+ const refreshHandlers = [];
247
+
248
+ // Separate handlers from other properties so they can be chained together.
249
+ // Features are applied at class creation time, so no existing handlers need
250
+ // to be preserved. This allows multiple features to handle the same namespace
251
+ // or participate in state refresh operations.
252
+ for (const feature of featureArray) {
253
+ if (feature.handlePushNotification) {
254
+ pushHandlers.push(feature.handlePushNotification);
255
+ }
256
+ if (feature.refreshState) {
257
+ refreshHandlers.push(feature.refreshState);
258
+ }
259
+
260
+ const { handlePushNotification: _handlePushNotification, refreshState: _refreshState, ...featureProperties } = feature;
261
+ Object.assign(DynamicDevice.prototype, featureProperties);
262
+ }
263
+
264
+ // Chain push handlers so multiple features can process notifications.
265
+ // The first handler that returns true stops the chain to avoid duplicate processing.
266
+ if (pushHandlers.length > 0) {
267
+ DynamicDevice.prototype.handlePushNotification = function (namespace, data) {
268
+ for (const handler of pushHandlers) {
269
+ if (handler.call(this, namespace, data)) {
270
+ return true;
271
+ }
272
+ }
273
+ return false;
274
+ };
275
+ }
276
+
277
+ // Chain refresh handlers so all features can update their state.
278
+ // All handlers are called sequentially since state refresh operations
279
+ // may depend on each other and must complete in order.
280
+ if (refreshHandlers.length > 0) {
281
+ DynamicDevice.prototype.refreshState = async function (timeout = null) {
282
+ for (const handler of refreshHandlers) {
283
+ await handler.call(this, timeout);
284
+ }
285
+ };
286
+ }
287
+
288
+ return DynamicDevice;
289
+ }
290
+
291
+ /**
292
+ * Builds a device instance from device info and abilities.
293
+ *
294
+ * Uses cached device classes when available, or dynamically creates a new class
295
+ * that includes only the features supported by the device's abilities. Hub devices
296
+ * are detected by the presence of a specific ability and use a different base class.
297
+ *
298
+ * @param {Object} deviceInfo - Device info from HTTP API (contains deviceType, hdwareVersion, fmwareVersion, etc.)
299
+ * @param {Object} abilities - Device abilities dictionary from `Appliance.System.Ability` namespace
300
+ * @param {MerossCloud} manager - MerossCloud manager instance
301
+ * @param {Array<Object>} [subDeviceList] - Optional list of subdevices for hub devices
302
+ * @returns {MerossDevice|MerossHubDevice} Device instance with appropriate features applied
303
+ */
304
+ function buildDevice(deviceInfo, abilities, manager, subDeviceList) {
305
+ const { deviceType } = deviceInfo;
306
+ const hardwareVersion = deviceInfo.hdwareVersion;
307
+ const firmwareVersion = deviceInfo.fmwareVersion;
308
+
309
+ // Hub detection must happen before class selection since hubs use a different base class
310
+ const isHub = abilities && typeof abilities === 'object' &&
311
+ HUB_DISCRIMINATING_ABILITY in abilities;
312
+
313
+ let DeviceClass = getCachedDeviceClass(deviceType, hardwareVersion, firmwareVersion);
314
+
315
+ if (!DeviceClass) {
316
+ // Lazy import device classes to avoid circular dependency
317
+ const { MerossDevice } = require('./controller/device');
318
+ const { MerossHubDevice } = require('./controller/hub-device');
319
+
320
+ const BaseClass = isHub ? MerossHubDevice : MerossDevice;
321
+
322
+ const typeKey = getTypeKey(deviceType, hardwareVersion, firmwareVersion);
323
+ DeviceClass = _buildDynamicClass(typeKey, abilities, BaseClass);
324
+
325
+ _dynamicTypes.set(typeKey, DeviceClass);
326
+ }
327
+
328
+ if (isHub) {
329
+ return new DeviceClass(manager, deviceInfo, subDeviceList);
330
+ } else {
331
+ return new DeviceClass(manager, deviceInfo);
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Maps subdevice type identifiers to their corresponding device class constructors.
337
+ *
338
+ * Different subdevice types require different class implementations because they
339
+ * expose different capabilities and API namespaces. This mapping enables the factory
340
+ * to instantiate the appropriate specialized class for each subdevice type.
341
+ *
342
+ * @private
343
+ */
344
+ const SUBDEVICE_MAPPING = {
345
+ 'mts100v3': require('./controller/subdevice').HubThermostatValve,
346
+ 'ms100': require('./controller/subdevice').HubTempHumSensor,
347
+ 'ms100f': require('./controller/subdevice').HubTempHumSensor,
348
+ 'ms130': require('./controller/subdevice').HubTempHumSensor,
349
+ 'ms405': require('./controller/subdevice').HubWaterLeakSensor,
350
+ 'ms400': require('./controller/subdevice').HubWaterLeakSensor,
351
+ 'ma151': require('./controller/subdevice').HubSmokeDetector
352
+ };
353
+
354
+ /**
355
+ * Maps subdevice types to the hub ability namespaces they require.
356
+ *
357
+ * Hubs expose abilities for all their subdevices, but each subdevice only needs
358
+ * access to namespaces relevant to its type. This mapping filters the hub's full
359
+ * ability set to prevent subdevices from accessing capabilities they don't support,
360
+ * reducing API surface and potential misuse.
361
+ *
362
+ * @private
363
+ */
364
+ const SUBDEVICE_ABILITY_MAPPING = {
365
+ // Temperature/Humidity sensors
366
+ 'ms100': ['Appliance.Hub.Sensor.TempHum', 'Appliance.Hub.Sensor.All'],
367
+ 'ms100f': ['Appliance.Hub.Sensor.TempHum', 'Appliance.Hub.Sensor.All'],
368
+ 'ms130': ['Appliance.Hub.Sensor.TempHum', 'Appliance.Hub.Sensor.All'],
369
+ // Smoke detectors
370
+ 'ma151': ['Appliance.Hub.Sensor.Smoke', 'Appliance.Hub.Sensor.All'],
371
+ // Water leak sensors
372
+ 'ms405': ['Appliance.Hub.Sensor.WaterLeak', 'Appliance.Hub.Sensor.All'],
373
+ 'ms400': ['Appliance.Hub.Sensor.WaterLeak', 'Appliance.Hub.Sensor.All'],
374
+ // Thermostat valves
375
+ 'mts100v3': ['Appliance.Hub.Mts100.All', 'Appliance.Hub.Mts100.Temperature', 'Appliance.Hub.Mts100.Mode', 'Appliance.Hub.Mts100.Adjust']
376
+ };
377
+
378
+ /**
379
+ * Creates a subdevice instance from subdevice information.
380
+ *
381
+ * Instantiates the appropriate subdevice class based on the subdevice type to ensure
382
+ * type-specific functionality is available. Falls back to the generic MerossSubDevice
383
+ * class for unknown types to maintain compatibility with new or unsupported subdevice
384
+ * types without breaking the application.
385
+ *
386
+ * @param {Object|HttpSubdeviceInfo} subdeviceInfo - Subdevice info from HTTP API (contains subDeviceType/type, id, etc.) or HttpSubdeviceInfo instance
387
+ * @param {string} hubUuid - UUID of the hub device that owns this subdevice
388
+ * @param {Object} hubAbilities - Hub's abilities dictionary (currently unused, reserved for future use)
389
+ * @param {MerossCloud} manager - MerossCloud manager instance
390
+ * @returns {MerossSubDevice} Subdevice instance (specific subclass or generic MerossSubDevice)
391
+ */
392
+ function buildSubdevice(subdeviceInfo, hubUuid, hubAbilities, manager) {
393
+ const HttpSubdeviceInfo = require('./model/http/subdevice');
394
+ const { MerossSubDevice } = require('./controller/subdevice');
395
+
396
+ const httpSubdeviceInfo = subdeviceInfo instanceof HttpSubdeviceInfo
397
+ ? subdeviceInfo
398
+ : HttpSubdeviceInfo.fromDict(subdeviceInfo);
399
+
400
+ const subdeviceId = httpSubdeviceInfo.subDeviceId;
401
+ const subdeviceType = httpSubdeviceInfo.subDeviceType || '';
402
+ const normalizedType = subdeviceType.toLowerCase();
403
+
404
+ const SubdeviceClass = SUBDEVICE_MAPPING[normalizedType];
405
+
406
+ const kwargs = {
407
+ subDeviceType: httpSubdeviceInfo.subDeviceType,
408
+ subDeviceName: httpSubdeviceInfo.subDeviceName
409
+ };
410
+
411
+ if (!SubdeviceClass) {
412
+ const logger = manager?.options?.logger || console.warn;
413
+ logger(`Unknown subdevice type: ${subdeviceType}. Using generic MerossSubDevice`);
414
+ return new MerossSubDevice(hubUuid, subdeviceId, manager, kwargs);
415
+ }
416
+
417
+ return new SubdeviceClass(hubUuid, subdeviceId, manager, kwargs);
418
+ }
419
+
420
+ /**
421
+ * Filters hub abilities to only those relevant for a specific subdevice type.
422
+ *
423
+ * Hubs expose abilities for all their subdevices, but each subdevice should only
424
+ * access namespaces it supports. This filtering prevents subdevices from attempting
425
+ * to use unsupported APIs and reduces the ability surface exposed to each subdevice.
426
+ *
427
+ * @param {string} subdeviceType - Subdevice type (e.g., 'ms130', 'ma151', 'mts100v3')
428
+ * @param {Object} hubAbilities - Hub's full abilities dictionary from `Appliance.System.Ability`
429
+ * @returns {Object} Filtered abilities object containing only namespaces relevant to the subdevice type
430
+ */
431
+ function getSubdeviceAbilities(subdeviceType, hubAbilities) {
432
+ if (!hubAbilities || typeof hubAbilities !== 'object') {
433
+ return {};
434
+ }
435
+
436
+ const normalizedType = (subdeviceType || '').toLowerCase();
437
+ const relevantNamespaces = SUBDEVICE_ABILITY_MAPPING[normalizedType];
438
+
439
+ if (!relevantNamespaces) {
440
+ return {};
441
+ }
442
+
443
+ const subdeviceAbilities = {};
444
+ for (const namespace of relevantNamespaces) {
445
+ if (hubAbilities[namespace]) {
446
+ subdeviceAbilities[namespace] = hubAbilities[namespace];
447
+ }
448
+ }
449
+
450
+ return subdeviceAbilities;
451
+ }
452
+
453
+ module.exports = {
454
+ getTypeKey,
455
+ getCachedDeviceClass,
456
+ buildDevice,
457
+ HUB_DISCRIMINATING_ABILITY,
458
+ SUBDEVICE_MAPPING,
459
+ SUBDEVICE_ABILITY_MAPPING,
460
+ buildSubdevice,
461
+ getSubdeviceAbilities
462
+ };
463
+
@@ -0,0 +1,138 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Represents an error budget for a single device
5
+ *
6
+ * Tracks the number of errors allowed within a time window. The budget decreases
7
+ * as errors occur and resets when the time window expires.
8
+ *
9
+ * @class ErrorBudget
10
+ * @private
11
+ */
12
+ class ErrorBudget {
13
+ /**
14
+ * Creates a new ErrorBudget instance
15
+ *
16
+ * @param {number} initialBudget - Initial error budget (number of errors allowed)
17
+ * @param {number} windowStart - Timestamp when the time window started (milliseconds)
18
+ */
19
+ constructor(initialBudget, windowStart) {
20
+ this.budget = initialBudget;
21
+ this.windowStart = windowStart;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Manages error budgets for multiple devices
27
+ *
28
+ * Tracks error budgets per device to prevent excessive errors from overwhelming
29
+ * devices or causing rate limiting. When a device's error budget is exhausted,
30
+ * LAN HTTP communication is disabled for that device until the time window expires.
31
+ *
32
+ * @class ErrorBudgetManager
33
+ */
34
+ class ErrorBudgetManager {
35
+ /**
36
+ * Creates a new ErrorBudgetManager instance
37
+ *
38
+ * @param {number} [maxErrors=1] - Maximum number of errors allowed per device per time window
39
+ * @param {number} [timeWindowMs=60000] - Time window in milliseconds (default: 60 seconds)
40
+ */
41
+ constructor(maxErrors = 1, timeWindowMs = 60000) {
42
+ this._devicesBudget = new Map();
43
+ this._window = timeWindowMs;
44
+ this._maxErrors = maxErrors;
45
+ }
46
+
47
+ /**
48
+ * Gets or creates an error budget for a device and updates the time window if expired
49
+ *
50
+ * Resets the budget when the time window expires to allow the device to recover
51
+ * from temporary error conditions. This prevents permanent blacklisting of devices
52
+ * that experience transient network issues.
53
+ *
54
+ * @param {string} deviceUuid - Device UUID
55
+ * @returns {ErrorBudget} Error budget for the device
56
+ * @private
57
+ */
58
+ _getUpdateBudgetWindow(deviceUuid) {
59
+ let devBudget = this._devicesBudget.get(deviceUuid);
60
+ const now = Date.now();
61
+
62
+ if (!devBudget) {
63
+ devBudget = new ErrorBudget(this._maxErrors, now);
64
+ this._devicesBudget.set(deviceUuid, devBudget);
65
+ }
66
+
67
+ if (now > (devBudget.windowStart + this._window)) {
68
+ devBudget.budget = this._maxErrors;
69
+ devBudget.windowStart = now;
70
+ }
71
+
72
+ return devBudget;
73
+ }
74
+
75
+ /**
76
+ * Notifies that an error occurred for a device
77
+ *
78
+ * Decrements the device's error budget. Once exhausted, LAN HTTP communication
79
+ * is disabled for the device until the time window resets, preventing further
80
+ * failed requests that could cause rate limiting or device instability.
81
+ *
82
+ * @param {string} deviceUuid - Device UUID
83
+ * @returns {void}
84
+ */
85
+ notifyError(deviceUuid) {
86
+ const devBudget = this._getUpdateBudgetWindow(deviceUuid);
87
+ if (devBudget.budget < 1) {
88
+ return;
89
+ }
90
+ devBudget.budget -= 1;
91
+ }
92
+
93
+ /**
94
+ * Checks if a device's error budget is exhausted.
95
+ *
96
+ * Used to determine whether LAN HTTP communication should be disabled for
97
+ * a device to prevent further failed requests that could cause rate limiting
98
+ * or device instability.
99
+ *
100
+ * @param {string} deviceUuid - Device UUID
101
+ * @returns {boolean} True if error budget is exhausted, false otherwise
102
+ */
103
+ isOutOfBudget(deviceUuid) {
104
+ const budget = this._getUpdateBudgetWindow(deviceUuid);
105
+ return budget.budget < 1;
106
+ }
107
+
108
+ /**
109
+ * Gets the current error budget for a device.
110
+ *
111
+ * Useful for monitoring or debugging purposes to see how many errors remain
112
+ * before LAN HTTP communication is disabled for the device.
113
+ *
114
+ * @param {string} deviceUuid - Device UUID
115
+ * @returns {number} Current error budget (number of errors remaining)
116
+ */
117
+ getBudget(deviceUuid) {
118
+ const budget = this._getUpdateBudgetWindow(deviceUuid);
119
+ return budget.budget;
120
+ }
121
+
122
+ /**
123
+ * Resets the error budget for a device
124
+ *
125
+ * Removes the device's error budget entry, immediately re-enabling LAN HTTP
126
+ * communication. Used when manual intervention is needed to recover from
127
+ * error budget exhaustion.
128
+ *
129
+ * @param {string} deviceUuid - Device UUID
130
+ * @returns {void}
131
+ */
132
+ resetBudget(deviceUuid) {
133
+ this._devicesBudget.delete(deviceUuid);
134
+ }
135
+ }
136
+
137
+ module.exports = ErrorBudgetManager;
138
+