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