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,310 @@
1
+ 'use strict';
2
+
3
+ const TimerType = require('../model/enums').TimerType;
4
+
5
+ /**
6
+ * Timer utility functions.
7
+ *
8
+ * Provides helper functions for creating and working with Meross timer configurations.
9
+ * Timers execute actions at specific times on specified days of the week, following
10
+ * Meross device timer format conventions.
11
+ *
12
+ * @module utilities/timer
13
+ */
14
+
15
+ /**
16
+ * Converts time to minutes since midnight.
17
+ *
18
+ * Meross devices store timer times as minutes since midnight (0-1439). This function
19
+ * normalizes various time formats (HH:MM strings, Date objects, or already-converted
20
+ * minutes) to this device-compatible format.
21
+ *
22
+ * @param {string|Date|number} time - Time in HH:MM format (24-hour), Date object, or minutes since midnight
23
+ * @returns {number} Minutes since midnight (0-1439)
24
+ * @throws {Error} If time format is invalid
25
+ * @example
26
+ * timeToMinutes('14:30'); // Returns 870
27
+ * timeToMinutes(new Date(2023, 0, 1, 14, 30)); // Returns 870 (14:30)
28
+ * timeToMinutes(870); // Returns 870 (already in minutes)
29
+ */
30
+ function timeToMinutes(time) {
31
+ if (typeof time === 'number') {
32
+ if (time >= 0 && time < 1440) {
33
+ return time;
34
+ }
35
+ throw new Error(`Invalid time value: ${time}. Must be 0-1439 minutes`);
36
+ }
37
+
38
+ if (time instanceof Date) {
39
+ return time.getHours() * 60 + time.getMinutes();
40
+ }
41
+
42
+ if (typeof time === 'string') {
43
+ const trimmed = time.trim();
44
+ const timePattern = /^([0-1]?[0-9]|2[0-3]):([0-5][0-9])$/;
45
+ if (!timePattern.test(trimmed)) {
46
+ throw new Error(`Invalid time format: "${time}". Expected HH:MM (24-hour format)`);
47
+ }
48
+ const [hours, minutes] = trimmed.split(':').map(Number);
49
+ return hours * 60 + minutes;
50
+ }
51
+
52
+ throw new Error(`Invalid time type: ${typeof time}. Expected string (HH:MM), Date, or number`);
53
+ }
54
+
55
+ /**
56
+ * Converts minutes since midnight to HH:MM format string.
57
+ *
58
+ * Converts the internal timer format (minutes since midnight) to a human-readable
59
+ * 24-hour time string for display purposes.
60
+ *
61
+ * @param {number} minutes - Minutes since midnight (0-1439)
62
+ * @returns {string} Time in HH:MM format (24-hour)
63
+ * @throws {Error} If minutes value is invalid
64
+ * @example
65
+ * minutesToTime(870); // Returns "14:30"
66
+ * minutesToTime(0); // Returns "00:00"
67
+ */
68
+ function minutesToTime(minutes) {
69
+ if (typeof minutes !== 'number' || minutes < 0 || minutes >= 1440) {
70
+ throw new Error(`Invalid minutes value: ${minutes}. Must be 0-1439`);
71
+ }
72
+ const hours = Math.floor(minutes / 60);
73
+ const mins = minutes % 60;
74
+ return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;
75
+ }
76
+
77
+ /**
78
+ * Gets bitmask for special day keywords.
79
+ *
80
+ * Converts human-readable day group keywords to their corresponding bitmask values
81
+ * used by Meross devices. Bit 0-6 represent Monday-Sunday, with bit 7 reserved for
82
+ * the repeat flag.
83
+ *
84
+ * @private
85
+ * @param {string} keyword - Special keyword ('weekday', 'weekend', 'daily', 'everyday')
86
+ * @returns {number|null} Bitmask value for the keyword, or null if not a special keyword
87
+ */
88
+ function getSpecialKeywordBitmask(keyword) {
89
+ const keywordMap = {
90
+ weekday: (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4),
91
+ weekend: (1 << 5) | (1 << 6),
92
+ daily: 0x7F,
93
+ everyday: 0x7F
94
+ };
95
+
96
+ return keywordMap[keyword] !== undefined ? keywordMap[keyword] : null;
97
+ }
98
+
99
+ /**
100
+ * Parses a single day value to day number.
101
+ *
102
+ * Converts day names (full or abbreviated) or day numbers to a consistent numeric
103
+ * format where 0=Monday, 1=Tuesday, etc. This matches Meross device day numbering.
104
+ *
105
+ * @private
106
+ * @param {string|number} day - Day value as number (0-6) or string (day name)
107
+ * @returns {number} Day number (0-6, where 0=Monday)
108
+ * @throws {Error} If day value is invalid
109
+ */
110
+ function parseDayToNumber(day) {
111
+ const dayMap = {
112
+ monday: 0,
113
+ tuesday: 1,
114
+ wednesday: 2,
115
+ thursday: 3,
116
+ friday: 4,
117
+ saturday: 5,
118
+ sunday: 6,
119
+ mon: 0,
120
+ tue: 1,
121
+ wed: 2,
122
+ thu: 3,
123
+ fri: 4,
124
+ sat: 5,
125
+ sun: 6
126
+ };
127
+
128
+ if (typeof day === 'number') {
129
+ if (day < 0 || day > 6) {
130
+ throw new Error(`Invalid day number: ${day}. Must be 0-6 (0=Monday)`);
131
+ }
132
+ return day;
133
+ }
134
+
135
+ if (typeof day === 'string') {
136
+ const normalized = day.toLowerCase();
137
+ if (dayMap[normalized] !== undefined) {
138
+ return dayMap[normalized];
139
+ }
140
+ throw new Error(`Invalid day name: "${day}". Use 'monday', 'tuesday', etc.`);
141
+ }
142
+
143
+ throw new Error(`Invalid day type: ${typeof day}. Expected string or number`);
144
+ }
145
+
146
+ /**
147
+ * Converts array of weekday names or numbers to week bitmask.
148
+ *
149
+ * Meross devices use a bitmask to represent selected days of the week, with bits 0-6
150
+ * representing Monday-Sunday and bit 7 indicating whether the timer repeats weekly.
151
+ * This function converts human-readable day lists to this device format.
152
+ *
153
+ * @param {Array<string|number>} days - Array of weekday names ('monday', 'tuesday', etc.) or numbers (0-6, where 0=Monday)
154
+ * @param {boolean} [repeat=true] - Whether to set the repeat bit (bit 7). Default: true
155
+ * @returns {number} Week bitmask with selected days and repeat bit
156
+ * @throws {Error} If day names are invalid
157
+ * @example
158
+ * daysToWeekMask(['monday', 'wednesday', 'friday']); // Returns bitmask for Mon, Wed, Fri + repeat
159
+ * daysToWeekMask([0, 2, 4]); // Same as above using numbers
160
+ * daysToWeekMask(['weekday']); // Returns Monday-Friday
161
+ * daysToWeekMask(['weekend']); // Returns Saturday-Sunday
162
+ */
163
+ function daysToWeekMask(days, repeat = true) {
164
+ if (!Array.isArray(days) || days.length === 0) {
165
+ throw new Error('Days must be a non-empty array');
166
+ }
167
+
168
+ let bitmask = 0;
169
+ const specialKeywords = new Set(['weekday', 'weekend', 'daily', 'everyday']);
170
+
171
+ for (const day of days) {
172
+ if (typeof day === 'string' && specialKeywords.has(day.toLowerCase())) {
173
+ const keywordBitmask = getSpecialKeywordBitmask(day.toLowerCase());
174
+ if (keywordBitmask !== null) {
175
+ bitmask |= keywordBitmask;
176
+ }
177
+ }
178
+ }
179
+
180
+ for (const day of days) {
181
+ if (typeof day === 'string' && specialKeywords.has(day.toLowerCase())) {
182
+ continue;
183
+ }
184
+
185
+ const dayNum = parseDayToNumber(day);
186
+ bitmask |= (1 << dayNum);
187
+ }
188
+
189
+ if (repeat) {
190
+ bitmask |= 128;
191
+ }
192
+
193
+ return bitmask;
194
+ }
195
+
196
+ /**
197
+ * Normalizes days input to week bitmask with repeat bit handling.
198
+ *
199
+ * Consolidates the logic for converting days to week bitmask used by both createTimer
200
+ * and createTrigger. Handles both numeric bitmask input (preserving or modifying the
201
+ * repeat bit) and array input (converting day names/numbers to bitmask).
202
+ *
203
+ * @param {Array<string|number>|number} days - Days of week (array of names/numbers) or week bitmask (number)
204
+ * @param {boolean} [repeat=true] - Whether to set the repeat bit (bit 7). Default: true
205
+ * @returns {number} Week bitmask with selected days and repeat bit set correctly
206
+ * @example
207
+ * normalizeWeekBitmask(['monday', 'wednesday', 'friday'], true); // Returns bitmask for Mon, Wed, Fri + repeat
208
+ * normalizeWeekBitmask(159, false); // Returns 31 (removes repeat bit from existing bitmask)
209
+ * normalizeWeekBitmask(31, true); // Returns 159 (adds repeat bit to existing bitmask)
210
+ */
211
+ function normalizeWeekBitmask(days, repeat = true) {
212
+ if (typeof days === 'number') {
213
+ let week = days;
214
+ if (repeat && (week & 128) === 0) {
215
+ week |= 128;
216
+ } else if (!repeat && (week & 128) !== 0) {
217
+ week &= ~128;
218
+ }
219
+ return week;
220
+ }
221
+
222
+ return daysToWeekMask(days, repeat);
223
+ }
224
+
225
+ /**
226
+ * Generates a unique timer ID.
227
+ *
228
+ * Creates a unique identifier for timers by combining the current timestamp (base36)
229
+ * with random characters. This ensures uniqueness even when multiple timers are created
230
+ * in rapid succession.
231
+ *
232
+ * @returns {string} Unique timer ID
233
+ */
234
+ function generateTimerId() {
235
+ return `${Date.now().toString(36)}${Math.random().toString(36).substr(2, 8)}`;
236
+ }
237
+
238
+ /**
239
+ * Creates a timer configuration object with sensible defaults.
240
+ *
241
+ * Builds a complete timer configuration object in the format expected by Meross devices,
242
+ * converting human-readable inputs (time strings, day names) to device-compatible formats
243
+ * (minutes since midnight, week bitmask). Applies sensible defaults for optional fields.
244
+ *
245
+ * @param {Object} options - Timer configuration options
246
+ * @param {string} [options.alias] - Timer name/alias
247
+ * @param {string|Date|number} [options.time='12:00'] - Time in HH:MM format, Date object, or minutes since midnight
248
+ * @param {Array<string|number>|number} [options.days] - Days of week (array of names/numbers) or week bitmask
249
+ * @param {boolean} [options.on=true] - Whether to turn device on (true) or off (false)
250
+ * @param {number} [options.type=TimerType.SINGLE_POINT_WEEKLY_CYCLE] - Timer type
251
+ * @param {number} [options.channel=0] - Channel number
252
+ * @param {boolean} [options.enabled=true] - Whether timer is enabled
253
+ * @param {boolean} [options.repeat=true] - Whether to set repeat bit (for days array input)
254
+ * @param {string} [options.id] - Timer ID (auto-generated if not provided)
255
+ * @returns {Object} Timer configuration object
256
+ * @example
257
+ * createTimer({
258
+ * alias: 'Morning Lights',
259
+ * time: '07:00',
260
+ * days: ['weekday'],
261
+ * on: true
262
+ * });
263
+ */
264
+ function createTimer(options = {}) {
265
+ const {
266
+ alias = 'My Timer',
267
+ time = '12:00',
268
+ days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
269
+ on = true,
270
+ type = TimerType.SINGLE_POINT_WEEKLY_CYCLE,
271
+ channel = 0,
272
+ enabled = true,
273
+ repeat = true,
274
+ id
275
+ } = options;
276
+
277
+ const timeMinutes = timeToMinutes(time);
278
+ const week = normalizeWeekBitmask(days, repeat);
279
+
280
+ const timerx = {
281
+ id: id || generateTimerId(),
282
+ channel,
283
+ type,
284
+ time: timeMinutes,
285
+ week,
286
+ duration: 0,
287
+ sunOffset: 0,
288
+ enable: enabled ? 1 : 0,
289
+ alias,
290
+ createTime: Math.floor(Date.now() / 1000),
291
+ extend: {
292
+ toggle: {
293
+ onoff: on ? 1 : 0,
294
+ lmTime: 0
295
+ }
296
+ }
297
+ };
298
+
299
+ return timerx;
300
+ }
301
+
302
+ module.exports = {
303
+ timeToMinutes,
304
+ minutesToTime,
305
+ daysToWeekMask,
306
+ normalizeWeekBitmask,
307
+ generateTimerId,
308
+ createTimer
309
+ };
310
+
@@ -0,0 +1,286 @@
1
+ 'use strict';
2
+
3
+ const TriggerType = require('../model/enums').TriggerType;
4
+ const { normalizeWeekBitmask, generateTimerId } = require('./timer');
5
+
6
+ /**
7
+ * Trigger utility functions.
8
+ *
9
+ * Provides helper functions for creating and working with Meross trigger configurations.
10
+ * Triggers are countdown timers that execute an action after a specified duration, unlike
11
+ * timers which execute at a specific time. Triggers count down from when they're activated.
12
+ *
13
+ * @module utilities/trigger
14
+ */
15
+
16
+ /**
17
+ * Parses numeric duration value.
18
+ *
19
+ * Validates that numeric duration values are non-negative, as negative durations
20
+ * are not meaningful for countdown timers.
21
+ *
22
+ * @private
23
+ * @param {number} value - Numeric duration in seconds
24
+ * @returns {number} Duration in seconds
25
+ * @throws {Error} If duration is negative
26
+ */
27
+ function parseNumericDuration(value) {
28
+ if (value < 0) {
29
+ throw new Error(`Invalid duration: ${value}. Duration must be non-negative`);
30
+ }
31
+ return value;
32
+ }
33
+
34
+ /**
35
+ * Parses duration string with unit suffix.
36
+ *
37
+ * Converts human-readable duration strings (e.g., "30m", "2h", "45s") to seconds.
38
+ * Supports seconds (s), minutes (m), and hours (h) units. Returns null if the format
39
+ * doesn't match, allowing other parsers to attempt conversion.
40
+ *
41
+ * @private
42
+ * @param {string} str - Duration string with unit suffix
43
+ * @returns {number|null} Duration in seconds, or null if format doesn't match
44
+ * @throws {Error} If format is invalid
45
+ */
46
+ function parseUnitSuffixDuration(str) {
47
+ const timeUnitMatch = str.match(/^(\d+)([smh])$/i);
48
+ if (!timeUnitMatch) {
49
+ return null;
50
+ }
51
+
52
+ const value = parseInt(timeUnitMatch[1], 10);
53
+ const unit = timeUnitMatch[2].toLowerCase();
54
+
55
+ if (unit === 's') {
56
+ return value;
57
+ } else if (unit === 'm') {
58
+ return value * 60;
59
+ } else if (unit === 'h') {
60
+ return value * 3600;
61
+ }
62
+
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * Validates time components for MM:SS format.
68
+ *
69
+ * Ensures minutes and seconds values are within valid ranges (minutes >= 0,
70
+ * seconds 0-59) before converting to total seconds.
71
+ *
72
+ * @private
73
+ * @param {number} minutes - Minutes value
74
+ * @param {number} seconds - Seconds value
75
+ * @param {string} originalDuration - Original duration string for error messages
76
+ * @throws {Error} If validation fails
77
+ */
78
+ function validateMMSS(minutes, seconds, originalDuration) {
79
+ if (isNaN(minutes) || isNaN(seconds) || minutes < 0 || seconds < 0 || seconds >= 60) {
80
+ throw new Error(`Invalid time format: "${originalDuration}". Expected MM:SS`);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Validates time components for HH:MM:SS format.
86
+ *
87
+ * Ensures hours, minutes, and seconds values are within valid ranges (hours >= 0,
88
+ * minutes 0-59, seconds 0-59) before converting to total seconds.
89
+ *
90
+ * @private
91
+ * @param {number} hours - Hours value
92
+ * @param {number} minutes - Minutes value
93
+ * @param {number} seconds - Seconds value
94
+ * @param {string} originalDuration - Original duration string for error messages
95
+ * @throws {Error} If validation fails
96
+ */
97
+ function validateHHMMSS(hours, minutes, seconds, originalDuration) {
98
+ if (isNaN(hours) || isNaN(minutes) || isNaN(seconds) ||
99
+ hours < 0 || minutes < 0 || minutes >= 60 || seconds < 0 || seconds >= 60) {
100
+ throw new Error(`Invalid time format: "${originalDuration}". Expected HH:MM:SS`);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Parses time string in "HH:MM:SS" or "MM:SS" format.
106
+ *
107
+ * Converts time-formatted duration strings to total seconds. Supports both full
108
+ * hour:minute:second format and abbreviated minute:second format. Returns null if
109
+ * the format doesn't match, allowing other parsers to attempt conversion.
110
+ *
111
+ * @private
112
+ * @param {string} str - Time string in "HH:MM:SS" or "MM:SS" format
113
+ * @param {string} originalDuration - Original duration string for error messages
114
+ * @returns {number|null} Duration in seconds, or null if format doesn't match
115
+ * @throws {Error} If format is invalid
116
+ */
117
+ function parseTimeStringDuration(str, originalDuration) {
118
+ const timePattern = /^(?:(\d+):)?(\d+):(\d+)$/;
119
+ if (!timePattern.test(str)) {
120
+ return null;
121
+ }
122
+
123
+ const parts = str.split(':');
124
+ if (parts.length === 2) {
125
+ const minutes = parseInt(parts[0], 10);
126
+ const seconds = parseInt(parts[1], 10);
127
+ validateMMSS(minutes, seconds, originalDuration);
128
+ return minutes * 60 + seconds;
129
+ } else if (parts.length === 3) {
130
+ const hours = parseInt(parts[0], 10);
131
+ const minutes = parseInt(parts[1], 10);
132
+ const seconds = parseInt(parts[2], 10);
133
+ validateHHMMSS(hours, minutes, seconds, originalDuration);
134
+ return hours * 3600 + minutes * 60 + seconds;
135
+ }
136
+
137
+ return null;
138
+ }
139
+
140
+ /**
141
+ * Converts duration to seconds.
142
+ *
143
+ * Normalizes various duration formats to a consistent seconds value. Supports numeric
144
+ * seconds, unit-suffixed strings (e.g., "30m", "2h"), and time-formatted strings
145
+ * (e.g., "HH:MM:SS", "MM:SS"). This allows users to specify durations in the most
146
+ * convenient format for their use case.
147
+ *
148
+ * @param {string|number} duration - Duration as seconds (number), "HH:MM:SS" string, "30m" (minutes), "2h" (hours), etc.
149
+ * @returns {number} Duration in seconds
150
+ * @throws {Error} If duration format is invalid
151
+ * @example
152
+ * durationToSeconds(600); // Returns 600
153
+ * durationToSeconds('10m'); // Returns 600 (10 minutes)
154
+ * durationToSeconds('1h'); // Returns 3600 (1 hour)
155
+ * durationToSeconds('1:30:00'); // Returns 5400 (1 hour 30 minutes)
156
+ * durationToSeconds('30:00'); // Returns 1800 (30 minutes)
157
+ */
158
+ function durationToSeconds(duration) {
159
+ if (typeof duration === 'number') {
160
+ return parseNumericDuration(duration);
161
+ }
162
+
163
+ if (typeof duration === 'string') {
164
+ const trimmed = duration.trim();
165
+
166
+ const unitSuffixResult = parseUnitSuffixDuration(trimmed);
167
+ if (unitSuffixResult !== null) {
168
+ return unitSuffixResult;
169
+ }
170
+
171
+ const timeStringResult = parseTimeStringDuration(trimmed, duration);
172
+ if (timeStringResult !== null) {
173
+ return timeStringResult;
174
+ }
175
+
176
+ throw new Error(`Invalid duration format: "${duration}". Expected seconds (number), "Xm"/"Xh", or "HH:MM:SS"/"MM:SS" format`);
177
+ }
178
+
179
+ throw new Error(`Invalid duration type: ${typeof duration}. Expected string or number`);
180
+ }
181
+
182
+ /**
183
+ * Converts seconds to human-readable duration string.
184
+ *
185
+ * Formats duration values for display purposes, using the most appropriate units
186
+ * (hours, minutes, seconds) and omitting zero components. Only shows seconds when
187
+ * the duration is less than an hour to keep output concise.
188
+ *
189
+ * @param {number} seconds - Duration in seconds
190
+ * @returns {string} Human-readable duration (e.g., "1h 30m", "45m", "30s")
191
+ * @throws {Error} If seconds value is invalid
192
+ * @example
193
+ * secondsToDuration(5400); // Returns "1h 30m"
194
+ * secondsToDuration(600); // Returns "10m"
195
+ * secondsToDuration(30); // Returns "30s"
196
+ */
197
+ function secondsToDuration(seconds) {
198
+ if (typeof seconds !== 'number' || seconds < 0) {
199
+ throw new Error(`Invalid seconds value: ${seconds}. Must be non-negative number`);
200
+ }
201
+
202
+ if (seconds === 0) {
203
+ return '0s';
204
+ }
205
+
206
+ const hours = Math.floor(seconds / 3600);
207
+ const minutes = Math.floor((seconds % 3600) / 60);
208
+ const secs = seconds % 60;
209
+
210
+ const parts = [];
211
+ if (hours > 0) {
212
+ parts.push(`${hours}h`);
213
+ }
214
+ if (minutes > 0) {
215
+ parts.push(`${minutes}m`);
216
+ }
217
+ if (secs > 0 && hours === 0) {
218
+ parts.push(`${secs}s`);
219
+ }
220
+
221
+ return parts.join(' ') || '0s';
222
+ }
223
+
224
+ /**
225
+ * Creates a trigger configuration object with sensible defaults.
226
+ *
227
+ * Builds a complete trigger configuration object in the format expected by Meross devices,
228
+ * converting human-readable inputs (duration strings, day names) to device-compatible formats
229
+ * (seconds, week bitmask). Triggers are countdown timers that execute an action after a
230
+ * specified duration, unlike timers which execute at specific times.
231
+ *
232
+ * @param {Object} options - Trigger configuration options
233
+ * @param {string} [options.alias] - Trigger name/alias
234
+ * @param {string|number} [options.duration=600] - Duration as seconds (number), "30m", "1h", or "HH:MM:SS" format. Default: 600 (10 minutes)
235
+ * @param {Array<string|number>|number} [options.days] - Days of week (array of names/numbers) or week bitmask. Default: all days
236
+ * @param {number} [options.type=TriggerType.SINGLE_POINT_WEEKLY_CYCLE] - Trigger type
237
+ * @param {number} [options.channel=0] - Channel number
238
+ * @param {boolean} [options.enabled=true] - Whether trigger is enabled
239
+ * @param {boolean} [options.repeat=true] - Whether to set repeat bit (for days array input)
240
+ * @param {string} [options.id] - Trigger ID (auto-generated if not provided)
241
+ * @returns {Object} Trigger configuration object
242
+ * @example
243
+ * createTrigger({
244
+ * alias: 'Auto-off after 30 minutes',
245
+ * duration: '30m',
246
+ * days: ['weekday'],
247
+ * channel: 0
248
+ * });
249
+ */
250
+ function createTrigger(options = {}) {
251
+ const {
252
+ alias = 'My Trigger',
253
+ duration = 600, // Default: 10 minutes
254
+ days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
255
+ type = TriggerType.SINGLE_POINT_WEEKLY_CYCLE,
256
+ channel = 0,
257
+ enabled = true,
258
+ repeat = true,
259
+ id
260
+ } = options;
261
+
262
+ const durationSeconds = durationToSeconds(duration);
263
+ const week = normalizeWeekBitmask(days, repeat);
264
+
265
+ const triggerx = {
266
+ id: id || generateTimerId(),
267
+ channel,
268
+ type,
269
+ enable: enabled ? 1 : 0,
270
+ alias,
271
+ createTime: Math.floor(Date.now() / 1000),
272
+ rule: {
273
+ duration: durationSeconds,
274
+ week
275
+ }
276
+ };
277
+
278
+ return triggerx;
279
+ }
280
+
281
+ module.exports = {
282
+ durationToSeconds,
283
+ secondsToDuration,
284
+ createTrigger
285
+ };
286
+
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "meross-iot",
3
+ "version": "0.1.0",
4
+ "description": "Control Meross cloud devices using nodejs",
5
+ "author": "Abe Haverkamp",
6
+ "contributors": [
7
+ "Ingo Fischer <iobroker@fischer-ka.de>"
8
+ ],
9
+ "homepage": "https://github.com/Doekse/merossiot#readme",
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "meross",
13
+ "iot",
14
+ "smart-home",
15
+ "home-automation",
16
+ "mqtt",
17
+ "wlan",
18
+ "smart-plug",
19
+ "smart-switch",
20
+ "smart-light",
21
+ "thermostat",
22
+ "garage-door",
23
+ "roller-shutter",
24
+ "homey",
25
+ "nodejs"
26
+ ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/Doekse/merossiot"
30
+ },
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "dependencies": {
35
+ "mqtt": "^5.14.1",
36
+ "uuid": "^10.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@alcalzone/release-script": "^3.8.0",
40
+ "@alcalzone/release-script-plugin-license": "^3.7.0",
41
+ "eslint": "^9.39.1"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/Doekse/merossiot/issues"
45
+ },
46
+ "main": "index.js",
47
+ "types": "index.d.ts",
48
+ "files": [
49
+ "index.js",
50
+ "index.d.ts",
51
+ "lib/",
52
+ "README.md",
53
+ "LICENSE",
54
+ "CHANGELOG.md"
55
+ ],
56
+ "scripts": {
57
+ "release": "release-script",
58
+ "prepublishOnly": "node -e \"require('./index.js')\" && echo 'Package validation passed'",
59
+ "publish:npm": "npm publish --access public",
60
+ "lint": "eslint .",
61
+ "lint:fix": "eslint . --fix"
62
+ },
63
+ "release-script": {
64
+ "changelog": {
65
+ "file": "CHANGELOG.md",
66
+ "template": "keepachangelog"
67
+ },
68
+ "npm": {
69
+ "publish": false
70
+ }
71
+ }
72
+ }
73
+