@homebridge-plugins/homebridge-meross 10.10.3 → 10.12.1-beta.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 +26 -0
- package/CLAUDE.md +74 -0
- package/README.md +1 -1
- package/config.schema.json +40 -0
- package/eslint.config.js +15 -11
- package/lib/device/hub-main.js +89 -0
- package/lib/device/hub-sprinkler.js +235 -0
- package/lib/device/index.js +2 -0
- package/lib/platform.js +6 -9
- package/lib/utils/constants.js +11 -5
- package/lib/utils/lang-en.js +0 -1
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@homebridge-plugins/homebridge-meross` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## v10.12.0 (2026-02-28)
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- support new models
|
|
10
|
+
- `MSS100` `MOP320` `MTS100` `MTS150P` `MST100` `MTS205` `P11` `R10` `R11` `R21` (beta)
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- dependency updates + maintenance
|
|
15
|
+
|
|
16
|
+
## v10.11.0 (2026-02-15)
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- add garage model `MSG150` (beta)
|
|
21
|
+
- add hub model `MSH450` (beta)
|
|
22
|
+
|
|
23
|
+
### Changes
|
|
24
|
+
|
|
25
|
+
- determine debug mode from `-D` flag
|
|
26
|
+
- updated dependencies
|
|
27
|
+
- updated dependencies + lint rules
|
|
28
|
+
- update workflow action versions
|
|
29
|
+
- fix deprecate past releases script
|
|
30
|
+
|
|
5
31
|
## v10.10.3 (2025-12-05)
|
|
6
32
|
|
|
7
33
|
### Changes
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
Homebridge plugin that integrates Meross smart home devices into Apple HomeKit. Supports 31 device types with cloud (MQTT), local (HTTP), and hybrid connection modes. Written in JavaScript (ES modules), no TypeScript compilation step.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
- **Lint**: `npm run lint` (eslint with zero warnings allowed)
|
|
12
|
+
- **Lint + fix**: `npm run lint:fix`
|
|
13
|
+
- **Install**: `npm install`
|
|
14
|
+
|
|
15
|
+
There are no automated tests. CI runs linting only (Node 20, 22, 24).
|
|
16
|
+
|
|
17
|
+
## Code Style
|
|
18
|
+
|
|
19
|
+
Uses `@antfu/eslint-config` with these key rules:
|
|
20
|
+
- Single quotes, 1tbs brace style
|
|
21
|
+
- Sorted imports/exports via `perfectionist` plugin (grouped: types, then builtins, externals, internals)
|
|
22
|
+
- `no-undef` enabled
|
|
23
|
+
- Zero warnings tolerance (`--max-warnings=0`)
|
|
24
|
+
|
|
25
|
+
## Architecture
|
|
26
|
+
|
|
27
|
+
### Entry Point
|
|
28
|
+
`lib/index.js` registers the platform class with Homebridge under the alias "Meross".
|
|
29
|
+
|
|
30
|
+
### Platform (`lib/platform.js`)
|
|
31
|
+
Central orchestrator (~1250 lines). Key responsibilities:
|
|
32
|
+
- **`constructor`**: Validates config, sets up Homebridge event listeners
|
|
33
|
+
- **`applyUserConfig`**: Parses and validates per-device config arrays (singleDevices, multiDevices, lightDevices, etc.)
|
|
34
|
+
- **`pluginSetup`**: Cloud login, device list retrieval, initializes all devices
|
|
35
|
+
- **`pluginShutdown`**: Closes MQTT connections, clears intervals
|
|
36
|
+
- **`initialiseDevice`**: Factory that maps device models to device classes using `platformConsts.models.*` lookups
|
|
37
|
+
- **`sendUpdate`**: Hybrid control - tries local HTTP first, falls back to cloud MQTT
|
|
38
|
+
|
|
39
|
+
### Device Classes (`lib/device/`)
|
|
40
|
+
Each file exports a single class for one device type. Common pattern:
|
|
41
|
+
- Constructor sets up HAP services/characteristics and event handlers
|
|
42
|
+
- Uses `PQueue` (concurrency: 1, interval: 250ms) for sequential request queuing
|
|
43
|
+
- `internalStateUpdate(value)` - handles HomeKit commands → device
|
|
44
|
+
- `requestUpdate(firstRun)` - polls device state → HomeKit
|
|
45
|
+
- `externalUpdate(params)` - handles MQTT push updates → HomeKit
|
|
46
|
+
- State caching to prevent redundant updates
|
|
47
|
+
|
|
48
|
+
Multi-channel devices (garage, outlets, switches) create sub-accessories per channel. The garage door (MSG200) uses a 3-accessory pattern: hidden main + visible sub-doors.
|
|
49
|
+
|
|
50
|
+
### Connection Layer (`lib/connection/`)
|
|
51
|
+
- **`http.js`**: Cloud REST API client. MD5-based auth signatures, spoofs iOS Meross app headers.
|
|
52
|
+
- **`mqtt.js`**: Cloud real-time client. Subscribes to `/app/{userId}/subscribe`, publishes commands, waits for response by messageId matching.
|
|
53
|
+
|
|
54
|
+
### Utilities (`lib/utils/`)
|
|
55
|
+
- **`constants.js`**: Default config values, min values, device model → type mappings (this is where new device models are registered)
|
|
56
|
+
- **`functions.js`**: Small helpers (encodeParams, generateRandomString, hasProperty, parseError, sleep)
|
|
57
|
+
- **`colour.js`**: RGB↔HSV conversion, color temperature handling
|
|
58
|
+
- **`custom-chars.js`** / **`eve-chars.js`**: Custom HomeKit characteristics
|
|
59
|
+
- **`lang-en.js`**: All user-facing log strings
|
|
60
|
+
|
|
61
|
+
### Fakegato (`lib/fakegato/`)
|
|
62
|
+
Eve app history service integration for home analytics.
|
|
63
|
+
|
|
64
|
+
## Adding a New Device Type
|
|
65
|
+
|
|
66
|
+
1. Add the model string to the appropriate array in `lib/utils/constants.js` under `models`
|
|
67
|
+
2. Create a new device class in `lib/device/` following the existing pattern (see `switch-single.js` for a simple example)
|
|
68
|
+
3. Export it from `lib/device/index.js`
|
|
69
|
+
4. Add the initialization branch in `platform.js` `initialiseDevice()` method
|
|
70
|
+
5. If the device needs per-device config, add allowed fields in `constants.js` `allowed` object and update `config.schema.json`
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
`config.schema.json` (~90KB) defines the Homebridge UI config form. Global settings include connection mode, cloud credentials, and refresh rates. Per-device arrays allow overriding connection type, IP address (for local control), model code, and device-specific options.
|
package/README.md
CHANGED
package/config.schema.json
CHANGED
|
@@ -181,6 +181,10 @@
|
|
|
181
181
|
"description": "The model of this device.",
|
|
182
182
|
"type": "string",
|
|
183
183
|
"oneOf": [
|
|
184
|
+
{
|
|
185
|
+
"title": "MSS100",
|
|
186
|
+
"enum": ["MSS100"]
|
|
187
|
+
},
|
|
184
188
|
{
|
|
185
189
|
"title": "MSS105",
|
|
186
190
|
"enum": ["MSS105"]
|
|
@@ -316,6 +320,18 @@
|
|
|
316
320
|
{
|
|
317
321
|
"title": "MSS810",
|
|
318
322
|
"enum": ["MSS810"]
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
"title": "P11",
|
|
326
|
+
"enum": ["P11"]
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
"title": "R10",
|
|
330
|
+
"enum": ["R10"]
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
"title": "R11",
|
|
334
|
+
"enum": ["R11"]
|
|
319
335
|
}
|
|
320
336
|
],
|
|
321
337
|
"condition": {
|
|
@@ -550,6 +566,14 @@
|
|
|
550
566
|
{
|
|
551
567
|
"title": "MSS630",
|
|
552
568
|
"enum": ["MSS630"]
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
"title": "MOP320",
|
|
572
|
+
"enum": ["MOP320"]
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
"title": "R21",
|
|
576
|
+
"enum": ["R21"]
|
|
553
577
|
}
|
|
554
578
|
],
|
|
555
579
|
"condition": {
|
|
@@ -1012,6 +1036,10 @@
|
|
|
1012
1036
|
{
|
|
1013
1037
|
"title": "MOD100",
|
|
1014
1038
|
"enum": ["MOD100"]
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
"title": "MOD150",
|
|
1042
|
+
"enum": ["MOD150"]
|
|
1015
1043
|
}
|
|
1016
1044
|
],
|
|
1017
1045
|
"condition": {
|
|
@@ -1312,6 +1340,10 @@
|
|
|
1312
1340
|
"title": "MTS200B",
|
|
1313
1341
|
"enum": ["MTS200B"]
|
|
1314
1342
|
},
|
|
1343
|
+
{
|
|
1344
|
+
"title": "MTS205",
|
|
1345
|
+
"enum": ["MTS205"]
|
|
1346
|
+
},
|
|
1315
1347
|
{
|
|
1316
1348
|
"title": "MTS960",
|
|
1317
1349
|
"enum": ["MTS960"]
|
|
@@ -1411,6 +1443,10 @@
|
|
|
1411
1443
|
"title": "MSG100",
|
|
1412
1444
|
"enum": ["MSG100"]
|
|
1413
1445
|
},
|
|
1446
|
+
{
|
|
1447
|
+
"title": "MSG150",
|
|
1448
|
+
"enum": ["MSG150"]
|
|
1449
|
+
},
|
|
1414
1450
|
{
|
|
1415
1451
|
"title": "MSG200",
|
|
1416
1452
|
"enum": ["MSG200"]
|
|
@@ -1730,6 +1766,10 @@
|
|
|
1730
1766
|
"title": "MSH400",
|
|
1731
1767
|
"enum": ["MSH400"]
|
|
1732
1768
|
},
|
|
1769
|
+
{
|
|
1770
|
+
"title": "MSH450",
|
|
1771
|
+
"enum": ["MSH450"]
|
|
1772
|
+
},
|
|
1733
1773
|
{
|
|
1734
1774
|
"title": "MS600",
|
|
1735
1775
|
"enum": ["MS600"]
|
package/eslint.config.js
CHANGED
|
@@ -8,29 +8,24 @@ export default antfu(
|
|
|
8
8
|
rules: {
|
|
9
9
|
'curly': ['error', 'multi-line'],
|
|
10
10
|
'new-cap': 'off',
|
|
11
|
-
'jsdoc/check-alignment': 'warn',
|
|
12
|
-
'jsdoc/check-line-alignment': 'warn',
|
|
13
|
-
'jsdoc/require-returns-check': 0,
|
|
14
|
-
'jsdoc/require-returns-description': 0,
|
|
15
11
|
'no-undef': 'error',
|
|
16
12
|
'perfectionist/sort-exports': 'error',
|
|
17
13
|
'perfectionist/sort-imports': [
|
|
18
14
|
'error',
|
|
19
15
|
{
|
|
20
16
|
groups: [
|
|
21
|
-
'type',
|
|
22
|
-
'
|
|
17
|
+
['type-builtin', 'type-external', 'type-internal'],
|
|
18
|
+
['type-parent', 'type-sibling', 'type-index'],
|
|
23
19
|
'builtin',
|
|
24
20
|
'external',
|
|
25
21
|
'internal',
|
|
26
|
-
['parent-type', 'sibling-type', 'index-type'],
|
|
27
22
|
['parent', 'sibling', 'index'],
|
|
28
|
-
'
|
|
23
|
+
'side-effect',
|
|
29
24
|
'unknown',
|
|
30
25
|
],
|
|
31
26
|
order: 'asc',
|
|
32
27
|
type: 'natural',
|
|
33
|
-
newlinesBetween:
|
|
28
|
+
newlinesBetween: 1,
|
|
34
29
|
},
|
|
35
30
|
],
|
|
36
31
|
'perfectionist/sort-named-exports': 'error',
|
|
@@ -41,8 +36,17 @@ export default antfu(
|
|
|
41
36
|
'style/quote-props': ['error', 'consistent-as-needed'],
|
|
42
37
|
'test/no-only-tests': 'error',
|
|
43
38
|
'unicorn/no-useless-spread': 'error',
|
|
44
|
-
'unused-imports/no-unused-vars':
|
|
39
|
+
'unused-imports/no-unused-vars': 0,
|
|
40
|
+
},
|
|
41
|
+
typescript: true,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
files: ['**/*.md'],
|
|
45
|
+
rules: {
|
|
46
|
+
'perfectionist/sort-exports': 'off',
|
|
47
|
+
'perfectionist/sort-imports': 'off',
|
|
48
|
+
'perfectionist/sort-named-exports': 'off',
|
|
49
|
+
'perfectionist/sort-named-imports': 'off',
|
|
45
50
|
},
|
|
46
|
-
typescript: false,
|
|
47
51
|
},
|
|
48
52
|
)
|
package/lib/device/hub-main.js
CHANGED
|
@@ -18,6 +18,7 @@ export default class {
|
|
|
18
18
|
// Set up variables from the accessory
|
|
19
19
|
this.accessory = accessory
|
|
20
20
|
this.mtsList = []
|
|
21
|
+
this.waterList = []
|
|
21
22
|
this.name = accessory.displayName
|
|
22
23
|
const cloudRefreshRate = hasProperty(platform.config, 'cloudRefreshRate')
|
|
23
24
|
? platform.config.cloudRefreshRate
|
|
@@ -118,6 +119,16 @@ export default class {
|
|
|
118
119
|
update.voltage = subdevice.ms100.voltage
|
|
119
120
|
}
|
|
120
121
|
subAcc.control.applyUpdate(update)
|
|
122
|
+
} else if (subdevice.mst) {
|
|
123
|
+
// MST100 sprinkler timer - extract onoff from digest
|
|
124
|
+
const update = {}
|
|
125
|
+
if (hasProperty(subdevice.mst, 'onoff')) {
|
|
126
|
+
update.onoff = subdevice.mst.onoff
|
|
127
|
+
}
|
|
128
|
+
if (hasProperty(subdevice.mst, 'voltage')) {
|
|
129
|
+
update.voltage = subdevice.mst.voltage
|
|
130
|
+
}
|
|
131
|
+
subAcc.control.applyUpdate(update)
|
|
121
132
|
} else if (subdevice.waterLeak) {
|
|
122
133
|
// Apply the update to the accessory
|
|
123
134
|
subAcc.control.applyUpdate(subdevice)
|
|
@@ -130,6 +141,11 @@ export default class {
|
|
|
130
141
|
if (hasProperty(subdevice, 'scheduleBMode')) {
|
|
131
142
|
this.mtsList.push(subdevice.id)
|
|
132
143
|
}
|
|
144
|
+
|
|
145
|
+
// Check to see if any MST (sprinkler) exist
|
|
146
|
+
if (hasProperty(subdevice, 'mst')) {
|
|
147
|
+
this.waterList.push(subdevice.id)
|
|
148
|
+
}
|
|
133
149
|
})
|
|
134
150
|
}
|
|
135
151
|
|
|
@@ -240,6 +256,51 @@ export default class {
|
|
|
240
256
|
})
|
|
241
257
|
}
|
|
242
258
|
}
|
|
259
|
+
|
|
260
|
+
// Request status for any MST (sprinkler) devices that exist
|
|
261
|
+
if (this.waterList.length > 0) {
|
|
262
|
+
const payload = {
|
|
263
|
+
control: this.waterList.map(id => ({ subId: id, channel: 0 })),
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Send the request
|
|
267
|
+
const res3 = await this.platform.sendUpdate(this.accessory, {
|
|
268
|
+
namespace: 'Appliance.Control.Water',
|
|
269
|
+
payload,
|
|
270
|
+
method: 'GET',
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
// Log the received data
|
|
274
|
+
this.accessory.logDebug(`${platformLang.incPoll}: ${JSON.stringify(res3.data)}`)
|
|
275
|
+
|
|
276
|
+
const data3 = res3.data.payload
|
|
277
|
+
if (data3.control && Array.isArray(data3.control)) {
|
|
278
|
+
data3.control.forEach((entry) => {
|
|
279
|
+
// Check whether the homebridge accessory this relates to exists
|
|
280
|
+
const subAcc = this.devicesInHB.get(
|
|
281
|
+
this.platform.api.hap.uuid.generate(this.accessory.context.serialNumber + entry.subId),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
// No need to continue if the accessory doesn't exist nor the receiver function
|
|
285
|
+
if (!subAcc || !subAcc.control || !subAcc.control.applyUpdate) {
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const toReturn = {}
|
|
290
|
+
if (hasProperty(entry, 'onoff')) {
|
|
291
|
+
toReturn.onoff = entry.onoff
|
|
292
|
+
}
|
|
293
|
+
if (hasProperty(entry, 'dura')) {
|
|
294
|
+
toReturn.dura = entry.dura
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Apply the update
|
|
298
|
+
if (Object.keys(toReturn).length > 0) {
|
|
299
|
+
subAcc.control.applyUpdate(toReturn)
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
}
|
|
243
304
|
})
|
|
244
305
|
} catch (err) {
|
|
245
306
|
const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
|
|
@@ -377,6 +438,34 @@ export default class {
|
|
|
377
438
|
})
|
|
378
439
|
}
|
|
379
440
|
|
|
441
|
+
// Water control updates (MST100 sprinkler)
|
|
442
|
+
if (data.control && Array.isArray(data.control)) {
|
|
443
|
+
data.control.forEach((entry) => {
|
|
444
|
+
// Check whether the homebridge accessory this relates to exists
|
|
445
|
+
const subAcc = this.devicesInHB.get(
|
|
446
|
+
this.platform.api.hap.uuid.generate(this.accessory.context.serialNumber + entry.subId),
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
// No need to continue if the accessory doesn't exist nor the receiver function
|
|
450
|
+
if (!subAcc || !subAcc.control || !subAcc.control.applyUpdate) {
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const toReturn = {}
|
|
455
|
+
if (hasProperty(entry, 'onoff')) {
|
|
456
|
+
toReturn.onoff = entry.onoff
|
|
457
|
+
}
|
|
458
|
+
if (hasProperty(entry, 'dura')) {
|
|
459
|
+
toReturn.dura = entry.dura
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Apply the update
|
|
463
|
+
if (Object.keys(toReturn).length > 0) {
|
|
464
|
+
subAcc.control.applyUpdate(toReturn)
|
|
465
|
+
}
|
|
466
|
+
})
|
|
467
|
+
}
|
|
468
|
+
|
|
380
469
|
// Leak sensor updates
|
|
381
470
|
if (data.waterLeak && Array.isArray(data.waterLeak)) {
|
|
382
471
|
data.waterLeak.forEach((entry) => {
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import PQueue from 'p-queue'
|
|
2
|
+
import { TimeoutError } from 'p-timeout'
|
|
3
|
+
|
|
4
|
+
import platformConsts from '../utils/constants.js'
|
|
5
|
+
import { hasProperty, parseError } from '../utils/functions.js'
|
|
6
|
+
import platformLang from '../utils/lang-en.js'
|
|
7
|
+
|
|
8
|
+
export default class {
|
|
9
|
+
constructor(platform, accessory, priAcc) {
|
|
10
|
+
// Set up variables from the platform
|
|
11
|
+
this.hapChar = platform.api.hap.Characteristic
|
|
12
|
+
this.hapErr = platform.api.hap.HapStatusError
|
|
13
|
+
this.hapServ = platform.api.hap.Service
|
|
14
|
+
this.platform = platform
|
|
15
|
+
|
|
16
|
+
// Set up variables from the accessory
|
|
17
|
+
this.accessory = accessory
|
|
18
|
+
this.lowBattThreshold = accessory.context.options.lowBattThreshold
|
|
19
|
+
? Math.min(accessory.context.options.lowBattThreshold, 100)
|
|
20
|
+
: platformConsts.defaultValues.lowBattThreshold
|
|
21
|
+
this.name = accessory.displayName
|
|
22
|
+
this.priAcc = priAcc
|
|
23
|
+
|
|
24
|
+
// Add the valve service if it doesn't already exist
|
|
25
|
+
this.service = this.accessory.getService(this.hapServ.Valve)
|
|
26
|
+
|| this.accessory.addService(this.hapServ.Valve)
|
|
27
|
+
|
|
28
|
+
// Set the valve type to irrigation
|
|
29
|
+
this.service.getCharacteristic(this.hapChar.ValveType).updateValue(1)
|
|
30
|
+
|
|
31
|
+
// Set up Active characteristic
|
|
32
|
+
this.service
|
|
33
|
+
.getCharacteristic(this.hapChar.Active)
|
|
34
|
+
.onSet(async value => this.internalStateUpdate(value))
|
|
35
|
+
this.cacheState = this.service.getCharacteristic(this.hapChar.Active).value
|
|
36
|
+
|
|
37
|
+
// Set up InUse characteristic (mirrors Active)
|
|
38
|
+
this.cacheInUse = this.service.getCharacteristic(this.hapChar.InUse).value
|
|
39
|
+
|
|
40
|
+
// Set up SetDuration characteristic
|
|
41
|
+
this.service
|
|
42
|
+
.getCharacteristic(this.hapChar.SetDuration)
|
|
43
|
+
.setProps({
|
|
44
|
+
minValue: 0,
|
|
45
|
+
maxValue: 43200,
|
|
46
|
+
minStep: 60,
|
|
47
|
+
})
|
|
48
|
+
.onSet(async value => this.internalDurationUpdate(value))
|
|
49
|
+
this.cacheDuration = this.service.getCharacteristic(this.hapChar.SetDuration).value || 3600
|
|
50
|
+
|
|
51
|
+
// Set up RemainingDuration characteristic
|
|
52
|
+
this.service
|
|
53
|
+
.getCharacteristic(this.hapChar.RemainingDuration)
|
|
54
|
+
.onGet(() => this.getRemainingDuration())
|
|
55
|
+
|
|
56
|
+
// Add the battery service if it doesn't already exist
|
|
57
|
+
this.battService = this.accessory.getService(this.hapServ.Battery)
|
|
58
|
+
|| this.accessory.addService(this.hapServ.Battery)
|
|
59
|
+
this.cacheBatt = this.battService.getCharacteristic(this.hapChar.BatteryLevel).value
|
|
60
|
+
|
|
61
|
+
// Activation timestamp for remaining duration calculation
|
|
62
|
+
this.activationTime = 0
|
|
63
|
+
this.durationTimer = null
|
|
64
|
+
|
|
65
|
+
// Create the queue used for sending device requests
|
|
66
|
+
this.updateInProgress = false
|
|
67
|
+
this.queue = new PQueue({
|
|
68
|
+
concurrency: 1,
|
|
69
|
+
interval: 250,
|
|
70
|
+
intervalCap: 1,
|
|
71
|
+
timeout: 10000,
|
|
72
|
+
throwOnTimeout: true,
|
|
73
|
+
})
|
|
74
|
+
this.queue.on('idle', () => {
|
|
75
|
+
this.updateInProgress = false
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Output the customised options to the log
|
|
79
|
+
const opts = JSON.stringify({
|
|
80
|
+
connection: this.accessory.context.connection,
|
|
81
|
+
lowBattThreshold: this.lowBattThreshold,
|
|
82
|
+
})
|
|
83
|
+
platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async internalStateUpdate(value) {
|
|
87
|
+
try {
|
|
88
|
+
// Add the request to the queue so updates are sent apart
|
|
89
|
+
await this.queue.add(async () => {
|
|
90
|
+
// Don't continue if the state is the same as before
|
|
91
|
+
if (value === this.cacheState) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// This flag stops the plugin from requesting updates while pending on others
|
|
96
|
+
this.updateInProgress = true
|
|
97
|
+
|
|
98
|
+
// Generate the payload and namespace
|
|
99
|
+
// MST100 uses Appliance.Control.Water with onoff: 1=on, 2=off
|
|
100
|
+
const namespace = 'Appliance.Control.Water'
|
|
101
|
+
const payload = {
|
|
102
|
+
control: [
|
|
103
|
+
{
|
|
104
|
+
subId: this.accessory.context.subSerialNumber,
|
|
105
|
+
channel: 0,
|
|
106
|
+
onoff: value ? 1 : 2,
|
|
107
|
+
dura: value ? this.cacheDuration : 0,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Use the platform function to send the update to the device
|
|
113
|
+
await this.platform.sendUpdate(this.priAcc, {
|
|
114
|
+
namespace,
|
|
115
|
+
payload,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// Update the cache and log the update has been successful
|
|
119
|
+
this.cacheState = value
|
|
120
|
+
this.service.updateCharacteristic(this.hapChar.InUse, value)
|
|
121
|
+
this.cacheInUse = value
|
|
122
|
+
|
|
123
|
+
if (value) {
|
|
124
|
+
this.startDurationTimer()
|
|
125
|
+
} else {
|
|
126
|
+
this.clearDurationTimer()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`)
|
|
130
|
+
})
|
|
131
|
+
} catch (err) {
|
|
132
|
+
// Catch any errors whilst updating the device
|
|
133
|
+
const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
|
|
134
|
+
this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
|
|
135
|
+
setTimeout(() => {
|
|
136
|
+
this.service.updateCharacteristic(this.hapChar.Active, this.cacheState)
|
|
137
|
+
}, 2000)
|
|
138
|
+
throw new this.hapErr(-70402)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
internalDurationUpdate(value) {
|
|
143
|
+
this.cacheDuration = value
|
|
144
|
+
|
|
145
|
+
// If currently active, restart the timer with the new duration
|
|
146
|
+
if (this.cacheState) {
|
|
147
|
+
this.startDurationTimer()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
getRemainingDuration() {
|
|
152
|
+
if (!this.cacheState || !this.activationTime) {
|
|
153
|
+
return 0
|
|
154
|
+
}
|
|
155
|
+
const elapsed = Math.floor((Date.now() - this.activationTime) / 1000)
|
|
156
|
+
const remaining = this.cacheDuration - elapsed
|
|
157
|
+
return Math.max(0, remaining)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
startDurationTimer() {
|
|
161
|
+
this.clearDurationTimer()
|
|
162
|
+
this.activationTime = Date.now()
|
|
163
|
+
|
|
164
|
+
if (this.cacheDuration > 0) {
|
|
165
|
+
this.durationTimer = setTimeout(() => {
|
|
166
|
+
// Auto-mark inactive when duration expires
|
|
167
|
+
this.cacheState = 0
|
|
168
|
+
this.cacheInUse = 0
|
|
169
|
+
this.service.updateCharacteristic(this.hapChar.Active, 0)
|
|
170
|
+
this.service.updateCharacteristic(this.hapChar.InUse, 0)
|
|
171
|
+
this.activationTime = 0
|
|
172
|
+
this.accessory.log(`${platformLang.curState} [off] (duration expired)`)
|
|
173
|
+
}, this.cacheDuration * 1000)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
clearDurationTimer() {
|
|
178
|
+
if (this.durationTimer) {
|
|
179
|
+
clearTimeout(this.durationTimer)
|
|
180
|
+
this.durationTimer = null
|
|
181
|
+
}
|
|
182
|
+
this.activationTime = 0
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
applyUpdate(data) {
|
|
186
|
+
try {
|
|
187
|
+
// Handle onoff state (1=on, 2=off)
|
|
188
|
+
if (hasProperty(data, 'onoff')) {
|
|
189
|
+
const newState = data.onoff === 1 ? 1 : 0
|
|
190
|
+
|
|
191
|
+
if (this.cacheState !== newState) {
|
|
192
|
+
this.service.updateCharacteristic(this.hapChar.Active, newState)
|
|
193
|
+
this.service.updateCharacteristic(this.hapChar.InUse, newState)
|
|
194
|
+
this.cacheState = newState
|
|
195
|
+
this.cacheInUse = newState
|
|
196
|
+
|
|
197
|
+
if (newState) {
|
|
198
|
+
this.startDurationTimer()
|
|
199
|
+
} else {
|
|
200
|
+
this.clearDurationTimer()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.accessory.log(`${platformLang.curState} [${newState ? 'on' : 'off'}]`)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Handle duration
|
|
208
|
+
if (hasProperty(data, 'dura')) {
|
|
209
|
+
const newDura = data.dura
|
|
210
|
+
if (newDura > 0 && newDura !== this.cacheDuration) {
|
|
211
|
+
this.cacheDuration = newDura
|
|
212
|
+
this.service.updateCharacteristic(this.hapChar.SetDuration, newDura)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Battery % from reported voltage
|
|
217
|
+
if (hasProperty(data, 'voltage')) {
|
|
218
|
+
// Scale from [2000, 3000] to [0, 100]
|
|
219
|
+
let newVoltage = Math.min(Math.max(data.voltage, 2000), 3000)
|
|
220
|
+
newVoltage = Math.round((newVoltage - 2000) / 10)
|
|
221
|
+
|
|
222
|
+
if (newVoltage !== this.cacheBatt) {
|
|
223
|
+
this.cacheBatt = newVoltage
|
|
224
|
+
this.battService.updateCharacteristic(this.hapChar.BatteryLevel, this.cacheBatt)
|
|
225
|
+
this.battService.updateCharacteristic(
|
|
226
|
+
this.hapChar.StatusLowBattery,
|
|
227
|
+
this.cacheBatt < this.lowBattThreshold ? 1 : 0,
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} catch (err) {
|
|
232
|
+
this.accessory.logWarn(`${platformLang.refFailed} ${parseError(err)}`)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
package/lib/device/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import deviceHubLeak from './hub-leak.js'
|
|
|
11
11
|
import deviceHubMain from './hub-main.js'
|
|
12
12
|
import deviceHubSensor from './hub-sensor.js'
|
|
13
13
|
import deviceHubSmoke from './hub-smoke.js'
|
|
14
|
+
import deviceHubSprinkler from './hub-sprinkler.js'
|
|
14
15
|
import deviceHubValve from './hub-valve.js'
|
|
15
16
|
import deviceHumidifier from './humidifier.js'
|
|
16
17
|
import deviceLightCCT from './light-cct.js'
|
|
@@ -43,6 +44,7 @@ export default {
|
|
|
43
44
|
deviceHubMain,
|
|
44
45
|
deviceHubSensor,
|
|
45
46
|
deviceHubSmoke,
|
|
47
|
+
deviceHubSprinkler,
|
|
46
48
|
deviceHubValve,
|
|
47
49
|
deviceHumidifier,
|
|
48
50
|
deviceLightCCT,
|
package/lib/platform.js
CHANGED
|
@@ -29,7 +29,7 @@ export default class {
|
|
|
29
29
|
try {
|
|
30
30
|
this.api = api
|
|
31
31
|
this.log = log
|
|
32
|
-
this.isBeta =
|
|
32
|
+
this.isBeta = process.argv.includes('-D')
|
|
33
33
|
this.cloudClient = false
|
|
34
34
|
this.deviceConf = {}
|
|
35
35
|
this.devicesInHB = new Map()
|
|
@@ -303,14 +303,6 @@ export default class {
|
|
|
303
303
|
if (this.isBeta) {
|
|
304
304
|
this.log.debug = this.log
|
|
305
305
|
this.log.debugWarn = this.log.warn
|
|
306
|
-
|
|
307
|
-
// Log that using a beta will generate a lot of debug logs
|
|
308
|
-
if (this.isBeta) {
|
|
309
|
-
const divide = '*'.repeat(platformLang.beta.length + 1) // don't forget the full stop (+1!)
|
|
310
|
-
this.log.warn(divide)
|
|
311
|
-
this.log.warn(`${platformLang.beta}.`)
|
|
312
|
-
this.log.warn(divide)
|
|
313
|
-
}
|
|
314
306
|
} else {
|
|
315
307
|
this.log.debug = () => {}
|
|
316
308
|
this.log.debugWarn = () => {}
|
|
@@ -944,8 +936,13 @@ export default class {
|
|
|
944
936
|
case 'MS400':
|
|
945
937
|
subAcc.control = new deviceTypes.deviceHubLeak(this, subAcc)
|
|
946
938
|
break
|
|
939
|
+
case 'MST100':
|
|
940
|
+
subAcc.control = new deviceTypes.deviceHubSprinkler(this, subAcc, accessory)
|
|
941
|
+
break
|
|
942
|
+
case 'MTS100':
|
|
947
943
|
case 'MTS100V3':
|
|
948
944
|
case 'MTS150':
|
|
945
|
+
case 'MTS150P':
|
|
949
946
|
subAcc.control = new deviceTypes.deviceHubValve(this, subAcc, accessory)
|
|
950
947
|
break
|
|
951
948
|
default:
|
package/lib/utils/constants.js
CHANGED
|
@@ -197,6 +197,7 @@ export default {
|
|
|
197
197
|
models: {
|
|
198
198
|
switchSingle: [
|
|
199
199
|
'HP110A',
|
|
200
|
+
'MSS100',
|
|
200
201
|
'MSS105',
|
|
201
202
|
'MSS110',
|
|
202
203
|
'MSS115',
|
|
@@ -231,6 +232,9 @@ export default {
|
|
|
231
232
|
'MSS710',
|
|
232
233
|
'MSS710R',
|
|
233
234
|
'MSS810',
|
|
235
|
+
'P11',
|
|
236
|
+
'R10',
|
|
237
|
+
'R11',
|
|
234
238
|
],
|
|
235
239
|
switchMulti: {
|
|
236
240
|
MSP843P: 5,
|
|
@@ -256,7 +260,9 @@ export default {
|
|
|
256
260
|
MSS620BR: 2,
|
|
257
261
|
MSS620R: 2,
|
|
258
262
|
MSS620S: 2,
|
|
263
|
+
MOP320: 3,
|
|
259
264
|
MSS630: 3,
|
|
265
|
+
R21: 2,
|
|
260
266
|
SP425EW: 4, // MSS425E
|
|
261
267
|
SP425FW: 4, // MSS425F
|
|
262
268
|
},
|
|
@@ -298,13 +304,13 @@ export default {
|
|
|
298
304
|
diffuser: ['MOD100', 'MOD150'],
|
|
299
305
|
purifier: ['MAP100'],
|
|
300
306
|
humidifier: ['MSXH0'],
|
|
301
|
-
garage: ['MSG100', 'MSG200'],
|
|
307
|
+
garage: ['MSG100', 'MSG150', 'MSG200'],
|
|
302
308
|
roller: ['MRS100'],
|
|
303
309
|
baby: ['HP110A', 'HP110AHK'],
|
|
304
|
-
thermostat: ['MTS200', 'MTS200B', 'MTS960'],
|
|
310
|
+
thermostat: ['MTS200', 'MTS200B', 'MTS205', 'MTS960'],
|
|
305
311
|
sensorPresence: ['MS600'],
|
|
306
|
-
hubMain: ['MSH300', 'MSH300HK', 'MSH400'],
|
|
307
|
-
hubSub: ['GS559A', 'MS100', 'MS100F', 'MS200', 'MS400', 'MTS100V3', 'MTS150'],
|
|
312
|
+
hubMain: ['MSH300', 'MSH300HK', 'MSH400', 'MSH450'],
|
|
313
|
+
hubSub: ['GS559A', 'MS100', 'MS100F', 'MS130', 'MS200', 'MS400', 'MST100', 'MTS100', 'MTS100V3', 'MTS150', 'MTS150P'],
|
|
308
314
|
template: [],
|
|
309
315
|
},
|
|
310
316
|
|
|
@@ -371,7 +377,7 @@ export default {
|
|
|
371
377
|
MSS315: ['9'], // https://github.com/homebridge-plugins/homebridge-meross/issues/537
|
|
372
378
|
},
|
|
373
379
|
|
|
374
|
-
noLocalControl: ['MSH300', 'MSH300HK', 'MSH400'],
|
|
380
|
+
noLocalControl: ['MSH300', 'MSH300HK', 'MSH400', 'MSH450'],
|
|
375
381
|
|
|
376
382
|
httpRetryCodes: ['ENOTFOUND', 'ETIMEDOUT', 'EAI_AGAIN', 'ECONNABORTED'],
|
|
377
383
|
}
|
package/lib/utils/lang-en.js
CHANGED
|
@@ -7,7 +7,6 @@ export default {
|
|
|
7
7
|
accTokenStoreErr: 'could not store access token as',
|
|
8
8
|
accTokenUserChange: 'username has changed',
|
|
9
9
|
alDisabled: 'adaptive lighting disabled due to change of colour detected',
|
|
10
|
-
beta: 'You are using a beta version of the plugin - you will experience more logging than normal',
|
|
11
10
|
brand: 'Meross Technology',
|
|
12
11
|
cfgDef: 'is not a valid number so using default of',
|
|
13
12
|
cfgDup: 'will be ignored since another entry with this ID already exists',
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@homebridge-plugins/homebridge-meross",
|
|
3
3
|
"alias": "Meross",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "10.
|
|
5
|
+
"version": "10.12.1-beta.0",
|
|
6
6
|
"description": "Homebridge plugin to integrate Meross devices into HomeKit.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "bwp91",
|
|
@@ -56,14 +56,14 @@
|
|
|
56
56
|
"lint:fix": "npm run lint -- --fix"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@homebridge/plugin-ui-utils": "^2.
|
|
60
|
-
"axios": "^1.13.
|
|
61
|
-
"mqtt": "^5.
|
|
59
|
+
"@homebridge/plugin-ui-utils": "^2.2.0",
|
|
60
|
+
"axios": "^1.13.6",
|
|
61
|
+
"mqtt": "^5.15.0",
|
|
62
62
|
"node-persist": "^4.0.4",
|
|
63
|
-
"p-queue": "^9.0
|
|
63
|
+
"p-queue": "^9.1.0",
|
|
64
64
|
"p-timeout": "^7.0.1"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
-
"@antfu/eslint-config": "^6.
|
|
67
|
+
"@antfu/eslint-config": "^7.6.1"
|
|
68
68
|
}
|
|
69
69
|
}
|