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,282 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Represents hardware information from device binding data.
5
+ *
6
+ * Encapsulates hardware metadata from device binding notifications. The API may return
7
+ * properties in either camelCase or snake_case format, so this class normalizes them
8
+ * to camelCase for consistent access.
9
+ *
10
+ * @class
11
+ * @example
12
+ * // Use fromDict() to create instances
13
+ * const hardwareInfo = HardwareInfo.fromDict({
14
+ * version: '1.0.0',
15
+ * uuid: '12345',
16
+ * type: 'mss310',
17
+ * sub_type: 'v1',
18
+ * mac_address: 'AA:BB:CC:DD:EE:FF',
19
+ * chip_type: 'ESP32'
20
+ * });
21
+ */
22
+ class HardwareInfo {
23
+ /**
24
+ * Creates a HardwareInfo instance from a dictionary object.
25
+ *
26
+ * Normalizes property names from both camelCase and snake_case formats to ensure
27
+ * consistent access regardless of API response format. Uses Object.create() instead
28
+ * of a constructor to allow static factory pattern.
29
+ *
30
+ * @static
31
+ * @param {Object} jsonDict - Raw API response object with any key format
32
+ * @param {string} [jsonDict.version] - Hardware version
33
+ * @param {string} [jsonDict.uuid] - Device UUID
34
+ * @param {string} [jsonDict.type] - Hardware type
35
+ * @param {string} [jsonDict.subType] - Hardware sub-type (camelCase)
36
+ * @param {string} [jsonDict.sub_type] - Hardware sub-type (snake_case)
37
+ * @param {string} [jsonDict.macAddress] - MAC address (camelCase)
38
+ * @param {string} [jsonDict.mac_address] - MAC address (snake_case)
39
+ * @param {string} [jsonDict.chipType] - Chip type (camelCase)
40
+ * @param {string} [jsonDict.chip_type] - Chip type (snake_case)
41
+ * @param {string} [jsonDict.chip_time] - Chip time (snake_case, mapped to chipType)
42
+ * @returns {HardwareInfo} New HardwareInfo instance
43
+ */
44
+ static fromDict(jsonDict) {
45
+ if (!jsonDict || typeof jsonDict !== 'object') {
46
+ return null;
47
+ }
48
+
49
+ // Map camelCase keys to their possible API variants (camelCase, snake_case)
50
+ const propertyMappings = {
51
+ version: ['version'],
52
+ uuid: ['uuid'],
53
+ type: ['type'],
54
+ subType: ['subType', 'sub_type'],
55
+ macAddress: ['macAddress', 'mac_address'],
56
+ chipType: ['chipType', 'chip_type', 'chip_time']
57
+ };
58
+
59
+ // Check each mapping in priority order to find the first non-null value
60
+ const normalized = {};
61
+ for (const [camelKey, alternatives] of Object.entries(propertyMappings)) {
62
+ for (const key of alternatives) {
63
+ if (jsonDict[key] !== null && jsonDict[key] !== undefined) {
64
+ normalized[camelKey] = jsonDict[key];
65
+ break;
66
+ }
67
+ }
68
+ }
69
+
70
+ // Use Object.create() to avoid constructor, allowing static factory pattern
71
+ const instance = Object.create(HardwareInfo.prototype);
72
+ instance.version = normalized.version || null;
73
+ instance.uuid = normalized.uuid || null;
74
+ instance.type = normalized.type || null;
75
+ instance.subType = normalized.subType || null;
76
+ instance.macAddress = normalized.macAddress || null;
77
+ instance.chipType = normalized.chipType || null;
78
+
79
+ return instance;
80
+ }
81
+
82
+ /**
83
+ * Converts the instance to a plain object dictionary with camelCase keys.
84
+ *
85
+ * @returns {Object} Plain object with camelCase property keys
86
+ */
87
+ toDict() {
88
+ return {
89
+ version: this.version,
90
+ uuid: this.uuid,
91
+ type: this.type,
92
+ subType: this.subType,
93
+ macAddress: this.macAddress,
94
+ chipType: this.chipType
95
+ };
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Represents firmware information from device binding data.
101
+ *
102
+ * Encapsulates firmware metadata from device binding notifications. The API may return
103
+ * properties in either camelCase or snake_case format, so this class normalizes them
104
+ * to camelCase for consistent access.
105
+ *
106
+ * @class
107
+ * @example
108
+ * // Use fromDict() to create instances
109
+ * const firmwareInfo = FirmwareInfo.fromDict({
110
+ * wifi_mac: 'AA:BB:CC:DD:EE:FF',
111
+ * version: '2.0.0',
112
+ * user_id: 'user123',
113
+ * server: 'mqtt.meross.com',
114
+ * port: 443,
115
+ * inner_ip: '192.168.1.100',
116
+ * compile_time: '2024-01-01'
117
+ * });
118
+ */
119
+ class FirmwareInfo {
120
+ /**
121
+ * Creates a FirmwareInfo instance from a dictionary object.
122
+ *
123
+ * Normalizes property names from both camelCase and snake_case formats to ensure
124
+ * consistent access regardless of API response format. Uses Object.create() instead
125
+ * of a constructor to allow static factory pattern.
126
+ *
127
+ * @static
128
+ * @param {Object} jsonDict - Raw API response object with any key format
129
+ * @param {string} [jsonDict.wifiMac] - WiFi MAC address (camelCase)
130
+ * @param {string} [jsonDict.wifi_mac] - WiFi MAC address (snake_case)
131
+ * @param {string} [jsonDict.version] - Firmware version
132
+ * @param {string} [jsonDict.userId] - User ID (camelCase)
133
+ * @param {string} [jsonDict.user_id] - User ID (snake_case)
134
+ * @param {string} [jsonDict.server] - Server address
135
+ * @param {number} [jsonDict.port] - Port number
136
+ * @param {string} [jsonDict.innerIp] - Inner IP address (camelCase)
137
+ * @param {string} [jsonDict.inner_ip] - Inner IP address (snake_case)
138
+ * @param {string} [jsonDict.compileTime] - Compile time (camelCase)
139
+ * @param {string} [jsonDict.compile_time] - Compile time (snake_case)
140
+ * @returns {FirmwareInfo} New FirmwareInfo instance
141
+ */
142
+ static fromDict(jsonDict) {
143
+ if (!jsonDict || typeof jsonDict !== 'object') {
144
+ return null;
145
+ }
146
+
147
+ // Map camelCase keys to their possible API variants (camelCase, snake_case)
148
+ const propertyMappings = {
149
+ wifiMac: ['wifiMac', 'wifi_mac'],
150
+ version: ['version'],
151
+ userId: ['userId', 'user_id'],
152
+ server: ['server'],
153
+ port: ['port'],
154
+ innerIp: ['innerIp', 'inner_ip'],
155
+ compileTime: ['compileTime', 'compile_time']
156
+ };
157
+
158
+ // Check each mapping in priority order to find the first non-null value
159
+ const normalized = {};
160
+ for (const [camelKey, alternatives] of Object.entries(propertyMappings)) {
161
+ for (const key of alternatives) {
162
+ if (jsonDict[key] !== null && jsonDict[key] !== undefined) {
163
+ normalized[camelKey] = jsonDict[key];
164
+ break;
165
+ }
166
+ }
167
+ }
168
+
169
+ // Use Object.create() to avoid constructor, allowing static factory pattern
170
+ const instance = Object.create(FirmwareInfo.prototype);
171
+ instance.wifiMac = normalized.wifiMac || null;
172
+ instance.version = normalized.version || null;
173
+ instance.userId = normalized.userId || null;
174
+ instance.server = normalized.server || null;
175
+ instance.port = normalized.port || null;
176
+ instance.innerIp = normalized.innerIp || null;
177
+ instance.compileTime = normalized.compileTime || null;
178
+
179
+ return instance;
180
+ }
181
+
182
+ /**
183
+ * Converts the instance to a plain object dictionary with camelCase keys.
184
+ *
185
+ * @returns {Object} Plain object with camelCase property keys
186
+ */
187
+ toDict() {
188
+ return {
189
+ wifiMac: this.wifiMac,
190
+ version: this.version,
191
+ userId: this.userId,
192
+ server: this.server,
193
+ port: this.port,
194
+ innerIp: this.innerIp,
195
+ compileTime: this.compileTime
196
+ };
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Represents time information from device binding data.
202
+ *
203
+ * Encapsulates time metadata from device binding notifications. The API may return
204
+ * properties in either camelCase or snake_case format, so this class normalizes them
205
+ * to camelCase for consistent access.
206
+ *
207
+ * @class
208
+ * @example
209
+ * // Use fromDict() to create instances
210
+ * const timeInfo = TimeInfo.fromDict({
211
+ * timezone: 'America/New_York',
212
+ * timestamp: 1234567890,
213
+ * time_rule: 'DST'
214
+ * });
215
+ */
216
+ class TimeInfo {
217
+ /**
218
+ * Creates a TimeInfo instance from a dictionary object.
219
+ *
220
+ * Normalizes property names from both camelCase and snake_case formats to ensure
221
+ * consistent access regardless of API response format. Uses Object.create() instead
222
+ * of a constructor to allow static factory pattern.
223
+ *
224
+ * @static
225
+ * @param {Object} jsonDict - Raw API response object with any key format
226
+ * @param {string} [jsonDict.timezone] - Timezone string
227
+ * @param {number} [jsonDict.timestamp] - Unix timestamp
228
+ * @param {string} [jsonDict.timeRule] - Time rule (camelCase)
229
+ * @param {string} [jsonDict.time_rule] - Time rule (snake_case)
230
+ * @returns {TimeInfo} New TimeInfo instance
231
+ */
232
+ static fromDict(jsonDict) {
233
+ if (!jsonDict || typeof jsonDict !== 'object') {
234
+ return null;
235
+ }
236
+
237
+ // Map camelCase keys to their possible API variants (camelCase, snake_case)
238
+ const propertyMappings = {
239
+ timezone: ['timezone'],
240
+ timestamp: ['timestamp'],
241
+ timeRule: ['timeRule', 'time_rule']
242
+ };
243
+
244
+ // Check each mapping in priority order to find the first non-null value
245
+ const normalized = {};
246
+ for (const [camelKey, alternatives] of Object.entries(propertyMappings)) {
247
+ for (const key of alternatives) {
248
+ if (jsonDict[key] !== null && jsonDict[key] !== undefined) {
249
+ normalized[camelKey] = jsonDict[key];
250
+ break;
251
+ }
252
+ }
253
+ }
254
+
255
+ // Use Object.create() to avoid constructor, allowing static factory pattern
256
+ const instance = Object.create(TimeInfo.prototype);
257
+ instance.timezone = normalized.timezone || null;
258
+ instance.timestamp = normalized.timestamp || null;
259
+ instance.timeRule = normalized.timeRule || null;
260
+
261
+ return instance;
262
+ }
263
+
264
+ /**
265
+ * Converts the instance to a plain object dictionary with camelCase keys.
266
+ *
267
+ * @returns {Object} Plain object with camelCase property keys
268
+ */
269
+ toDict() {
270
+ return {
271
+ timezone: this.timezone,
272
+ timestamp: this.timestamp,
273
+ timeRule: this.timeRule
274
+ };
275
+ }
276
+ }
277
+
278
+ module.exports = {
279
+ HardwareInfo,
280
+ FirmwareInfo,
281
+ TimeInfo
282
+ };
@@ -0,0 +1,100 @@
1
+ 'use strict';
2
+
3
+ const GenericPushNotification = require('./generic');
4
+
5
+ /**
6
+ * Push notification for diffuser light state changes.
7
+ *
8
+ * Emitted when a diffuser device's light settings change (on/off, mode, color, brightness).
9
+ * Contains the updated light state for one or more channels.
10
+ *
11
+ * @class
12
+ * @extends GenericPushNotification
13
+ * @example
14
+ * device.on('pushNotification', (notification) => {
15
+ * if (notification instanceof DiffuserLightPushNotification) {
16
+ * const lightData = notification.lightData;
17
+ * lightData.forEach(light => {
18
+ * console.log(`Channel ${light.channel} light is ${light.onoff === 1 ? 'on' : 'off'}`);
19
+ * });
20
+ * }
21
+ * });
22
+ */
23
+ class DiffuserLightPushNotification extends GenericPushNotification {
24
+ /**
25
+ * Creates a new DiffuserLightPushNotification instance.
26
+ *
27
+ * @param {string} originatingDeviceUuid - UUID of the device that sent the notification
28
+ * @param {Object} rawData - Raw notification data from the device
29
+ * @param {Object|Array} [rawData.light] - Light state data (single object or array)
30
+ * @param {number} [rawData.light.channel] - Channel number
31
+ * @param {number} [rawData.light.onoff] - On/off state (0=off, 1=on)
32
+ * @param {number} [rawData.light.mode] - Light mode
33
+ * @param {number} [rawData.light.rgb] - RGB color value
34
+ * @param {number} [rawData.light.luminance] - Brightness value
35
+ */
36
+ constructor(originatingDeviceUuid, rawData) {
37
+ super('Appliance.Control.Diffuser.Light', originatingDeviceUuid, rawData);
38
+
39
+ // Devices may send single objects or arrays; normalize to array for consistent processing
40
+ const lightRaw = rawData?.light;
41
+ const light = GenericPushNotification.normalizeToArray(lightRaw);
42
+
43
+ // Update rawData so routing logic receives normalized structure
44
+ if (rawData && lightRaw !== light) {
45
+ rawData.light = light;
46
+ }
47
+
48
+ this._lightData = light;
49
+ }
50
+
51
+ /**
52
+ * Gets the light state data array.
53
+ *
54
+ * @returns {Array} Array of light state objects (empty array if no data)
55
+ */
56
+ get lightData() {
57
+ return this._lightData;
58
+ }
59
+
60
+ /**
61
+ * Extracts diffuser light changes from this notification.
62
+ *
63
+ * Converts raw device data format (onoff, mode, rgb, luminance) to normalized change
64
+ * format used by subscription managers. Maps onoff (0/1) to boolean isOn for consistency.
65
+ *
66
+ * @returns {Object} Changes object with light state, e.g., { diffuserLight: { 0: {...} } }
67
+ */
68
+ extractChanges() {
69
+ const changes = {};
70
+ if (!this._lightData || this._lightData.length === 0) {
71
+ return changes;
72
+ }
73
+
74
+ changes.diffuserLight = {};
75
+ this._lightData.forEach(item => {
76
+ const channel = item.channel !== undefined ? item.channel : 0;
77
+ const lightChange = {};
78
+ if (item.onoff !== undefined) {
79
+ lightChange.isOn = item.onoff === 1;
80
+ }
81
+ if (item.mode !== undefined) {
82
+ lightChange.mode = item.mode;
83
+ }
84
+ if (item.rgb !== undefined) {
85
+ lightChange.rgb = item.rgb;
86
+ }
87
+ if (item.luminance !== undefined) {
88
+ lightChange.luminance = item.luminance;
89
+ }
90
+ if (Object.keys(lightChange).length > 0) {
91
+ changes.diffuserLight[channel] = lightChange;
92
+ }
93
+ });
94
+
95
+ return changes;
96
+ }
97
+ }
98
+
99
+ module.exports = DiffuserLightPushNotification;
100
+
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ const GenericPushNotification = require('./generic');
4
+
5
+ /**
6
+ * Push notification for diffuser spray state changes.
7
+ *
8
+ * Emitted when a diffuser device's spray mode changes.
9
+ * Contains the updated spray state for one or more channels.
10
+ *
11
+ * @class
12
+ * @extends GenericPushNotification
13
+ * @example
14
+ * device.on('pushNotification', (notification) => {
15
+ * if (notification instanceof DiffuserSprayPushNotification) {
16
+ * const sprayData = notification.sprayData;
17
+ * sprayData.forEach(spray => {
18
+ * console.log(`Channel ${spray.channel} spray mode: ${spray.mode}`);
19
+ * });
20
+ * }
21
+ * });
22
+ */
23
+ class DiffuserSprayPushNotification extends GenericPushNotification {
24
+ /**
25
+ * Creates a new DiffuserSprayPushNotification instance.
26
+ *
27
+ * @param {string} originatingDeviceUuid - UUID of the device that sent the notification
28
+ * @param {Object} rawData - Raw notification data from the device
29
+ * @param {Object|Array} [rawData.spray] - Spray state data (single object or array)
30
+ * @param {number} [rawData.spray.channel] - Channel number
31
+ * @param {number} [rawData.spray.mode] - Spray mode value
32
+ */
33
+ constructor(originatingDeviceUuid, rawData) {
34
+ super('Appliance.Control.Diffuser.Spray', originatingDeviceUuid, rawData);
35
+
36
+ // Devices may send single objects or arrays; normalize to array for consistent processing
37
+ const sprayRaw = rawData?.spray;
38
+ const spray = GenericPushNotification.normalizeToArray(sprayRaw);
39
+
40
+ // Update rawData so routing logic receives normalized structure
41
+ if (rawData && sprayRaw !== spray) {
42
+ rawData.spray = spray;
43
+ }
44
+
45
+ this._sprayData = spray;
46
+ }
47
+
48
+ /**
49
+ * Gets the spray state data array.
50
+ *
51
+ * @returns {Array} Array of spray state objects (empty array if no data)
52
+ */
53
+ get sprayData() {
54
+ return this._sprayData;
55
+ }
56
+
57
+ /**
58
+ * Extracts diffuser spray changes from this notification.
59
+ *
60
+ * Converts raw device data format to normalized change format used by subscription managers.
61
+ *
62
+ * @returns {Object} Changes object with spray state, e.g., { diffuserSpray: { 0: { mode: 1 } } }
63
+ */
64
+ extractChanges() {
65
+ const changes = {};
66
+ if (!this._sprayData || this._sprayData.length === 0) {
67
+ return changes;
68
+ }
69
+
70
+ changes.diffuserSpray = {};
71
+ this._sprayData.forEach(item => {
72
+ const channel = item.channel !== undefined ? item.channel : 0;
73
+ if (item.mode !== undefined) {
74
+ changes.diffuserSpray[channel] = { mode: item.mode };
75
+ }
76
+ });
77
+
78
+ return changes;
79
+ }
80
+ }
81
+
82
+ module.exports = DiffuserSprayPushNotification;
83
+
@@ -0,0 +1,229 @@
1
+ 'use strict';
2
+
3
+ const GenericPushNotification = require('./generic');
4
+ const OnlinePushNotification = require('./online');
5
+ const AlarmPushNotification = require('./alarm');
6
+ const BindPushNotification = require('./bind');
7
+ const UnbindPushNotification = require('./unbind');
8
+ const WaterLeakPushNotification = require('./water-leak');
9
+ const HubOnlinePushNotification = require('./hub-online');
10
+ const HubToggleXPushNotification = require('./hub-togglex');
11
+ const HubBatteryPushNotification = require('./hub-battery');
12
+ const HubSensorAllPushNotification = require('./hub-sensor-all');
13
+ const HubSensorTempHumPushNotification = require('./hub-sensor-temphum');
14
+ const HubSensorAlertPushNotification = require('./hub-sensor-alert');
15
+ const HubSensorSmokePushNotification = require('./hub-sensor-smoke');
16
+ const HubMts100AllPushNotification = require('./hub-mts100-all');
17
+ const HubMts100ModePushNotification = require('./hub-mts100-mode');
18
+ const HubMts100TemperaturePushNotification = require('./hub-mts100-temperature');
19
+ const HubSubdeviceListPushNotification = require('./hub-subdevicelist');
20
+ const SensorLatestXPushNotification = require('./sensor-latestx');
21
+ const TimerXPushNotification = require('./timerx');
22
+ const TriggerXPushNotification = require('./triggerx');
23
+ const ToggleXPushNotification = require('./togglex');
24
+ const PresenceStudyPushNotification = require('./presence-study');
25
+ const DiffuserLightPushNotification = require('./diffuser-light');
26
+ const DiffuserSprayPushNotification = require('./diffuser-spray');
27
+
28
+ const PUSH_NOTIFICATION_BINDING = {
29
+ 'Appliance.System.Online': OnlinePushNotification,
30
+ 'Appliance.Control.Alarm': AlarmPushNotification,
31
+ 'Appliance.Control.Bind': BindPushNotification,
32
+ 'Appliance.Control.Unbind': UnbindPushNotification,
33
+ 'Appliance.Control.ToggleX': ToggleXPushNotification,
34
+ 'Appliance.Control.TimerX': TimerXPushNotification,
35
+ 'Appliance.Control.TriggerX': TriggerXPushNotification,
36
+ 'Appliance.Hub.Sensor.WaterLeak': WaterLeakPushNotification,
37
+ 'Appliance.Hub.Online': HubOnlinePushNotification,
38
+ 'Appliance.Hub.ToggleX': HubToggleXPushNotification,
39
+ 'Appliance.Hub.Battery': HubBatteryPushNotification,
40
+ 'Appliance.Hub.Sensor.All': HubSensorAllPushNotification,
41
+ 'Appliance.Hub.Sensor.TempHum': HubSensorTempHumPushNotification,
42
+ 'Appliance.Hub.Sensor.Alert': HubSensorAlertPushNotification,
43
+ 'Appliance.Hub.Sensor.Smoke': HubSensorSmokePushNotification,
44
+ 'Appliance.Hub.Mts100.All': HubMts100AllPushNotification,
45
+ 'Appliance.Hub.Mts100.Mode': HubMts100ModePushNotification,
46
+ 'Appliance.Hub.Mts100.Temperature': HubMts100TemperaturePushNotification,
47
+ 'Appliance.Hub.SubdeviceList': HubSubdeviceListPushNotification,
48
+ 'Appliance.Control.Sensor.LatestX': SensorLatestXPushNotification,
49
+ 'Appliance.Control.Presence.Study': PresenceStudyPushNotification,
50
+ 'Appliance.Control.Diffuser.Light': DiffuserLightPushNotification,
51
+ 'Appliance.Control.Diffuser.Spray': DiffuserSprayPushNotification
52
+ };
53
+
54
+ /**
55
+ * Maps hub notification namespaces to their corresponding data keys in raw payloads.
56
+ *
57
+ * Hub notifications use different top-level keys in their payloads (e.g., 'online', 'togglex', 'battery').
58
+ * This mapping allows extraction of the correct data array for routing to subdevices.
59
+ */
60
+ const HUB_NAMESPACE_DATA_KEY_MAP = {
61
+ 'Appliance.Hub.Online': 'online',
62
+ 'Appliance.Hub.ToggleX': 'togglex',
63
+ 'Appliance.Hub.Battery': 'battery',
64
+ 'Appliance.Hub.Sensor.All': 'all',
65
+ 'Appliance.Hub.Sensor.TempHum': 'tempHum',
66
+ 'Appliance.Hub.Sensor.Alert': 'alert',
67
+ 'Appliance.Hub.Sensor.Smoke': 'smokeAlarm',
68
+ 'Appliance.Hub.Sensor.WaterLeak': 'waterLeak',
69
+ 'Appliance.Hub.Mts100.All': 'all',
70
+ 'Appliance.Hub.Mts100.Mode': 'mode',
71
+ 'Appliance.Hub.Mts100.Temperature': 'temperature',
72
+ 'Appliance.Hub.SubdeviceList': 'subdeviceList',
73
+ 'Appliance.Control.Sensor.LatestX': 'latest'
74
+ };
75
+
76
+ /**
77
+ * Strategy functions for extracting data arrays from raw payloads.
78
+ *
79
+ * Some hub notifications have non-standard data structures that don't follow the standard
80
+ * namespace-to-key mapping. These strategies handle special cases like nested structures
81
+ * or inconsistent property names. Data normalization (single object to array) happens
82
+ * at parse time in constructors.
83
+ */
84
+ const DATA_EXTRACTION_STRATEGIES = {
85
+ /**
86
+ * Extracts subdevice list data.
87
+ *
88
+ * Handles the inconsistent structure where subdevices may be nested in a 'subdevice'
89
+ * property or directly in 'subdeviceList'.
90
+ */
91
+ 'Appliance.Hub.SubdeviceList': (rawData) => {
92
+ if (rawData.subdeviceList) {
93
+ return rawData.subdeviceList.subdevice || rawData.subdeviceList;
94
+ }
95
+ return rawData.subdeviceList;
96
+ }
97
+ };
98
+
99
+ /**
100
+ * Parses a push notification from raw MQTT message data.
101
+ *
102
+ * Factory function that instantiates the appropriate notification class based on namespace.
103
+ * Falls back to GenericPushNotification if no specific class exists or if instantiation fails,
104
+ * ensuring all notifications can be handled even for unknown or malformed payloads.
105
+ *
106
+ * @param {string} namespace - The namespace of the push notification (e.g., 'Appliance.Control.ToggleX')
107
+ * @param {Object} messagePayload - The raw message payload from MQTT
108
+ * @param {string} deviceUuid - The UUID of the device that originated the notification
109
+ * @returns {GenericPushNotification|null} The parsed notification object, or null if invalid
110
+ * @example
111
+ * const notification = parsePushNotification(
112
+ * 'Appliance.Control.ToggleX',
113
+ * { togglex: [{ channel: 0, onoff: 1 }] },
114
+ * 'device-uuid-123'
115
+ * );
116
+ * if (notification instanceof ToggleXPushNotification) {
117
+ * console.log('Toggle state changed');
118
+ * }
119
+ */
120
+ function parsePushNotification(namespace, messagePayload, deviceUuid) {
121
+ if (!namespace || typeof namespace !== 'string') {
122
+ return null;
123
+ }
124
+
125
+ if (!deviceUuid || typeof deviceUuid !== 'string') {
126
+ return null;
127
+ }
128
+
129
+ const NotificationClass = PUSH_NOTIFICATION_BINDING[namespace];
130
+
131
+ if (NotificationClass) {
132
+ try {
133
+ return new NotificationClass(deviceUuid, messagePayload);
134
+ } catch (error) {
135
+ // Fall back to generic notification if specific class fails to parse
136
+ return new GenericPushNotification(namespace, deviceUuid, messagePayload);
137
+ }
138
+ }
139
+
140
+ // Return generic notification for unmapped namespaces (unknown notification types)
141
+ return new GenericPushNotification(namespace, deviceUuid, messagePayload);
142
+ }
143
+
144
+ /**
145
+ * Extracts the data array from a raw payload based on namespace.
146
+ *
147
+ * Uses namespace-specific extraction strategies for non-standard structures, otherwise
148
+ * falls back to the standard data key mapping. Returns null if extraction fails.
149
+ *
150
+ * @param {string} namespace - The namespace of the push notification
151
+ * @param {Object} rawData - The raw data payload
152
+ * @returns {Array|null} Data array or null if extraction fails
153
+ * @private
154
+ */
155
+ function extractDataArray(namespace, rawData) {
156
+ // Check for custom extraction strategy first (handles non-standard structures)
157
+ const extractionStrategy = DATA_EXTRACTION_STRATEGIES[namespace];
158
+ if (extractionStrategy) {
159
+ return extractionStrategy(rawData);
160
+ }
161
+
162
+ // Fall back to standard namespace-to-key mapping
163
+ const dataKey = HUB_NAMESPACE_DATA_KEY_MAP[namespace];
164
+ if (!dataKey) {
165
+ return null;
166
+ }
167
+
168
+ return rawData[dataKey];
169
+ }
170
+
171
+ /**
172
+ * Routes a hub push notification to the appropriate subdevices.
173
+ *
174
+ * Extracts subdevice data from a hub notification and forwards it to the corresponding
175
+ * subdevice instances. Each subdevice processes the notification asynchronously via
176
+ * handleSubdeviceNotification. Skips unregistered subdevices and logs warnings.
177
+ *
178
+ * @param {GenericPushNotification} notification - The push notification instance
179
+ * @param {MerossHubDevice} hubDevice - The hub device instance
180
+ * @example
181
+ * const notification = parsePushNotification(namespace, payload, hubUuid);
182
+ * if (notification.namespace.startsWith('Appliance.Hub.')) {
183
+ * routeToSubdevices(notification, hubDevice);
184
+ * }
185
+ */
186
+ function routeToSubdevices(notification, hubDevice) {
187
+ if (!notification || !hubDevice || typeof hubDevice.getSubdevice !== 'function') {
188
+ return;
189
+ }
190
+
191
+ const { namespace } = notification;
192
+ const rawData = notification.rawData || {};
193
+
194
+ // Extract data array using namespace-specific extraction logic
195
+ const dataArray = extractDataArray(namespace, rawData);
196
+
197
+ if (!Array.isArray(dataArray)) {
198
+ return;
199
+ }
200
+
201
+ for (const item of dataArray) {
202
+ // Handle inconsistent subdevice ID field names across notification types
203
+ const subdeviceId = item.subId || item.id;
204
+ if (!subdeviceId) {
205
+ continue;
206
+ }
207
+
208
+ const subdevice = hubDevice.getSubdevice(subdeviceId);
209
+ if (!subdevice) {
210
+ const logger = hubDevice.cloudInst?.options?.logger || console.warn;
211
+ logger(`Received update for subdevice (id ${subdeviceId}) that has not been registered with hub ${hubDevice.uuid}. Update will be skipped.`);
212
+ continue;
213
+ }
214
+
215
+ // Process asynchronously and catch errors to prevent one failure from blocking others
216
+ if (typeof subdevice.handleSubdeviceNotification === 'function') {
217
+ subdevice.handleSubdeviceNotification(namespace, item).catch(err => {
218
+ const logger = hubDevice.cloudInst?.options?.logger || console.error;
219
+ logger(`Error routing hub ${namespace} notification to subdevice ${subdeviceId}: ${err.message}`);
220
+ });
221
+ }
222
+ }
223
+ }
224
+
225
+ module.exports = {
226
+ parsePushNotification,
227
+ routeToSubdevices
228
+ };
229
+