@homebridge-plugins/homebridge-meross 10.8.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 +1346 -0
- package/LICENSE +21 -0
- package/README.md +68 -0
- package/config.schema.json +2066 -0
- package/eslint.config.js +49 -0
- package/lib/connection/http.js +345 -0
- package/lib/connection/mqtt.js +174 -0
- package/lib/device/baby.js +532 -0
- package/lib/device/cooler-single.js +447 -0
- package/lib/device/diffuser.js +730 -0
- package/lib/device/fan.js +530 -0
- package/lib/device/garage-main.js +225 -0
- package/lib/device/garage-single.js +495 -0
- package/lib/device/garage-sub.js +376 -0
- package/lib/device/heater-single.js +445 -0
- package/lib/device/hub-contact.js +56 -0
- package/lib/device/hub-leak.js +86 -0
- package/lib/device/hub-main.js +403 -0
- package/lib/device/hub-sensor.js +115 -0
- package/lib/device/hub-smoke.js +40 -0
- package/lib/device/hub-valve.js +377 -0
- package/lib/device/humidifier.js +521 -0
- package/lib/device/index.js +63 -0
- package/lib/device/light-cct.js +474 -0
- package/lib/device/light-dimmer.js +312 -0
- package/lib/device/light-rgb.js +528 -0
- package/lib/device/outlet-multi.js +383 -0
- package/lib/device/outlet-single.js +405 -0
- package/lib/device/power-strip.js +282 -0
- package/lib/device/purifier-single.js +372 -0
- package/lib/device/purifier.js +403 -0
- package/lib/device/roller-location.js +317 -0
- package/lib/device/roller.js +234 -0
- package/lib/device/sensor-presence.js +201 -0
- package/lib/device/switch-multi.js +403 -0
- package/lib/device/switch-single.js +371 -0
- package/lib/device/template.js +177 -0
- package/lib/device/thermostat.js +493 -0
- package/lib/fakegato/LICENSE +21 -0
- package/lib/fakegato/fakegato-history.js +814 -0
- package/lib/fakegato/fakegato-storage.js +108 -0
- package/lib/fakegato/fakegato-timer.js +125 -0
- package/lib/fakegato/uuid.js +27 -0
- package/lib/homebridge-ui/public/index.html +316 -0
- package/lib/homebridge-ui/server.js +10 -0
- package/lib/index.js +8 -0
- package/lib/platform.js +1256 -0
- package/lib/utils/colour.js +581 -0
- package/lib/utils/constants.js +377 -0
- package/lib/utils/custom-chars.js +165 -0
- package/lib/utils/eve-chars.js +130 -0
- package/lib/utils/functions.js +39 -0
- package/lib/utils/lang-en.js +114 -0
- package/package.json +70 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import PQueue from 'p-queue'
|
|
2
|
+
import { TimeoutError } from 'p-timeout'
|
|
3
|
+
|
|
4
|
+
import platformConsts from '../utils/constants.js'
|
|
5
|
+
import { generateRandomString, 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.eveChar = platform.eveChar
|
|
12
|
+
this.hapChar = platform.api.hap.Characteristic
|
|
13
|
+
this.hapErr = platform.api.hap.HapStatusError
|
|
14
|
+
this.hapServ = platform.api.hap.Service
|
|
15
|
+
this.platform = platform
|
|
16
|
+
|
|
17
|
+
// Set up variables from the accessory
|
|
18
|
+
this.accessory = accessory
|
|
19
|
+
this.operationTime = this.accessory.context.options.garageDoorOpeningTime
|
|
20
|
+
|| platformConsts.defaultValues.garageDoorOpeningTime
|
|
21
|
+
this.name = accessory.displayName
|
|
22
|
+
this.priAcc = priAcc
|
|
23
|
+
this.states = {
|
|
24
|
+
0: 'open',
|
|
25
|
+
1: 'closed',
|
|
26
|
+
2: 'opening',
|
|
27
|
+
3: 'closing',
|
|
28
|
+
4: 'stopped',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Add the garage door service if it doesn't already exist
|
|
32
|
+
this.service = this.accessory.getService(this.hapServ.GarageDoorOpener)
|
|
33
|
+
|| this.accessory.addService(this.hapServ.GarageDoorOpener)
|
|
34
|
+
|
|
35
|
+
// Add some extra Eve characteristics
|
|
36
|
+
if (!this.service.testCharacteristic(this.eveChar.LastActivation)) {
|
|
37
|
+
this.service.addCharacteristic(this.eveChar.LastActivation)
|
|
38
|
+
}
|
|
39
|
+
if (!this.service.testCharacteristic(this.eveChar.ResetTotal)) {
|
|
40
|
+
this.service.addCharacteristic(this.eveChar.ResetTotal)
|
|
41
|
+
}
|
|
42
|
+
if (!this.service.testCharacteristic(this.eveChar.TimesOpened)) {
|
|
43
|
+
this.service.addCharacteristic(this.eveChar.TimesOpened)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Add the set handler to the garage door target state characteristic
|
|
47
|
+
this.service
|
|
48
|
+
.getCharacteristic(this.hapChar.TargetDoorState)
|
|
49
|
+
.onSet(value => this.internalTargetUpdate(value))
|
|
50
|
+
this.cacheTarget = this.service.getCharacteristic(this.hapChar.TargetDoorState).value
|
|
51
|
+
this.cacheCurrent = this.service.getCharacteristic(this.hapChar.CurrentDoorState).value
|
|
52
|
+
|
|
53
|
+
// Add the set handler to the garage door reset total characteristic
|
|
54
|
+
this.service.getCharacteristic(this.eveChar.ResetTotal).onSet(() => {
|
|
55
|
+
this.service.updateCharacteristic(this.eveChar.TimesOpened, 0)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// Update the obstruction detected to false on plugin load
|
|
59
|
+
this.service.updateCharacteristic(this.hapChar.ObstructionDetected, false)
|
|
60
|
+
|
|
61
|
+
// Pass the accessory to Fakegato to set up with Eve
|
|
62
|
+
this.accessory.eveService = new platform.eveService('door', this.accessory, { log: () => {} })
|
|
63
|
+
this.accessory.eveService.addEntry({ status: this.cacheCurrent === 0 ? 0 : 1 })
|
|
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
|
+
garageDoorOpeningTime: this.operationTime,
|
|
82
|
+
hideChannels: accessory.context.options.hideChannels,
|
|
83
|
+
})
|
|
84
|
+
platform.log('[%s] %s %s.', this.name, platformLang.devInitOpts, opts)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async internalTargetUpdate(value) {
|
|
88
|
+
// Add the request to the queue so updates are sent apart
|
|
89
|
+
try {
|
|
90
|
+
await this.queue.add(async () => {
|
|
91
|
+
let action
|
|
92
|
+
let newTarget = value
|
|
93
|
+
let newCurrent
|
|
94
|
+
if (value === 1) {
|
|
95
|
+
// Request to close the garage door
|
|
96
|
+
if (this.cacheCurrent === 0) {
|
|
97
|
+
// The door is currently open
|
|
98
|
+
// ACTION: close the door
|
|
99
|
+
action = 'close'
|
|
100
|
+
|
|
101
|
+
// Mark the current door state as closing
|
|
102
|
+
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 3)
|
|
103
|
+
newCurrent = 3
|
|
104
|
+
} else if (this.cacheCurrent === 1) {
|
|
105
|
+
// The door is currently closed
|
|
106
|
+
// ACTION: none
|
|
107
|
+
// Mark the current door state as closed
|
|
108
|
+
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 1)
|
|
109
|
+
newCurrent = 1
|
|
110
|
+
} else if (this.cacheCurrent === 2) {
|
|
111
|
+
// The door is currently opening
|
|
112
|
+
// ACTION: close the door
|
|
113
|
+
action = 'close'
|
|
114
|
+
|
|
115
|
+
// Mark the target state as close and current door state as closing
|
|
116
|
+
this.service.updateCharacteristic(this.hapChar.TargetDoorState, 1)
|
|
117
|
+
newTarget = 1
|
|
118
|
+
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 3)
|
|
119
|
+
newCurrent = 3
|
|
120
|
+
} else if (this.cacheCurrent === 3) {
|
|
121
|
+
// The door is currently closing
|
|
122
|
+
// ACTION: none
|
|
123
|
+
// Mark the current door state as closing
|
|
124
|
+
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 3)
|
|
125
|
+
newCurrent = 3
|
|
126
|
+
}
|
|
127
|
+
} else if (value === 0) {
|
|
128
|
+
// Request to open the door
|
|
129
|
+
if (this.cacheCurrent === 0) {
|
|
130
|
+
// The door is currently open
|
|
131
|
+
// ACTION: none
|
|
132
|
+
// Mark the current door state as open
|
|
133
|
+
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 0)
|
|
134
|
+
newCurrent = 0
|
|
135
|
+
} else if (this.cacheCurrent === 1) {
|
|
136
|
+
// The door is currently closed
|
|
137
|
+
// ACTION: open the door
|
|
138
|
+
action = 'open'
|
|
139
|
+
|
|
140
|
+
// Mark the current door state as opening
|
|
141
|
+
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 2)
|
|
142
|
+
newCurrent = 2
|
|
143
|
+
} else if (this.cacheCurrent === 2) {
|
|
144
|
+
// The door is currently opening
|
|
145
|
+
// ACTION: none
|
|
146
|
+
|
|
147
|
+
// Mark the current door state as opening
|
|
148
|
+
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 2)
|
|
149
|
+
newCurrent = 2
|
|
150
|
+
} else if (this.cacheCurrent === 3) {
|
|
151
|
+
// The door is currently closing
|
|
152
|
+
// ACTION: open the door
|
|
153
|
+
action = 'open'
|
|
154
|
+
|
|
155
|
+
// Mark the target state as open and current state as opening
|
|
156
|
+
this.service.updateCharacteristic(this.hapChar.TargetDoorState, 0)
|
|
157
|
+
newTarget = 0
|
|
158
|
+
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 2)
|
|
159
|
+
newCurrent = 2
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Only send an update if we need to
|
|
164
|
+
if (action) {
|
|
165
|
+
this.ignoreIncoming = true
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
this.ignoreIncoming = false
|
|
168
|
+
}, 3000)
|
|
169
|
+
|
|
170
|
+
// Generate the payload and namespace for the correct device model
|
|
171
|
+
const namespace = 'Appliance.GarageDoor.State'
|
|
172
|
+
const payload = {
|
|
173
|
+
state: {
|
|
174
|
+
channel: this.accessory.context.channel,
|
|
175
|
+
open: action === 'open' ? 1 : 0,
|
|
176
|
+
uuid: this.accessory.context.serialNumber,
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Use the platform function to send the update to the device
|
|
181
|
+
await this.platform.sendUpdate(this.priAcc, {
|
|
182
|
+
namespace,
|
|
183
|
+
payload,
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Update the cache target state if different
|
|
188
|
+
if (this.cacheTarget !== newTarget) {
|
|
189
|
+
this.cacheTarget = newTarget
|
|
190
|
+
this.accessory.log(`${platformLang.curTarg} [${this.states[this.cacheTarget]}]`)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Update the cache current state if different
|
|
194
|
+
if (this.cacheCurrent !== newCurrent) {
|
|
195
|
+
this.cacheCurrent = newCurrent
|
|
196
|
+
this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/*
|
|
200
|
+
CASE: garage has been opened
|
|
201
|
+
target has been set to [open] and current has been set to [opening]
|
|
202
|
+
wait for the operation time to elapse and set the current to [open]
|
|
203
|
+
*/
|
|
204
|
+
if (action === 'open') {
|
|
205
|
+
const updateKey = generateRandomString(5)
|
|
206
|
+
this.updateKey = updateKey
|
|
207
|
+
|
|
208
|
+
// Update the Eve times opened characteristic
|
|
209
|
+
this.accessory.eveService.addEntry({ status: 0 })
|
|
210
|
+
this.service.updateCharacteristic(
|
|
211
|
+
this.eveChar.TimesOpened,
|
|
212
|
+
this.service.getCharacteristic(this.eveChar.TimesOpened).value + 1,
|
|
213
|
+
)
|
|
214
|
+
const initialTime = this.accessory.eveService.getInitialTime()
|
|
215
|
+
this.service.updateCharacteristic(
|
|
216
|
+
this.eveChar.LastActivation,
|
|
217
|
+
Math.round(new Date().valueOf() / 1000) - initialTime,
|
|
218
|
+
)
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
if (updateKey !== this.updateKey) {
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
if (this.service.getCharacteristic(this.hapChar.CurrentDoorState).value !== 2) {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 0)
|
|
227
|
+
this.cacheCurrent = 0
|
|
228
|
+
this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
|
|
229
|
+
}, this.operationTime * 1000)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/*
|
|
233
|
+
CASE: garage has been closed
|
|
234
|
+
target has been set to [close] and current has been set to [closing]
|
|
235
|
+
wait for the plugin to get a definite closed response from Meross
|
|
236
|
+
For security reasons, I don't want to rely on operation time for the garage to
|
|
237
|
+
definitely show as closed
|
|
238
|
+
Set a timer for operation time plus 15 seconds, and if the garage is still closing then
|
|
239
|
+
mark target and current state as open
|
|
240
|
+
Also setup quicker polling every 3 seconds when in local mode to get the closed status
|
|
241
|
+
*/
|
|
242
|
+
if (action === 'close') {
|
|
243
|
+
const updateKey = generateRandomString(5)
|
|
244
|
+
if (this.accessory.context.connection === 'local') {
|
|
245
|
+
this.extremePolling = setInterval(() => this.priAcc.control.requestUpdate(), 3000)
|
|
246
|
+
}
|
|
247
|
+
this.updateKey = updateKey
|
|
248
|
+
setTimeout(() => {
|
|
249
|
+
if (updateKey !== this.updateKey) {
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
if (this.service.getCharacteristic(this.hapChar.CurrentDoorState).value !== 3) {
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
this.service.updateCharacteristic(this.hapChar.TargetDoorState, 0)
|
|
256
|
+
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 0)
|
|
257
|
+
this.cacheTarget = 0
|
|
258
|
+
this.cacheCurrent = 0
|
|
259
|
+
this.accessory.log(`${platformLang.curTarg} [${this.states[this.cacheTarget]}]`)
|
|
260
|
+
this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
|
|
261
|
+
|
|
262
|
+
// Cancel any 'extreme' polling intervals from setting the garage to close
|
|
263
|
+
if (this.extremePolling) {
|
|
264
|
+
clearInterval(this.extremePolling)
|
|
265
|
+
this.extremePolling = false
|
|
266
|
+
}
|
|
267
|
+
}, (this.operationTime + 15) * 1000)
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
} catch (err) {
|
|
271
|
+
// Catch any errors whilst updating the device
|
|
272
|
+
const eText = err instanceof TimeoutError ? platformLang.timeout : parseError(err)
|
|
273
|
+
this.accessory.logWarn(`${platformLang.sendFailed} ${eText}`)
|
|
274
|
+
setTimeout(() => {
|
|
275
|
+
this.service.updateCharacteristic(this.hapChar.TargetDoorState, this.cacheTarget)
|
|
276
|
+
}, 2000)
|
|
277
|
+
this.service.updateCharacteristic(this.hapChar.TargetDoorState, new this.hapErr(-70402))
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
applyUpdate(data) {
|
|
282
|
+
// data will be in the format {"channel":1,"doorEnable":1,"open":0,"lmTime":1628623166}
|
|
283
|
+
// Don't bother whilst the ignore incoming is set to true
|
|
284
|
+
if (this.ignoreIncoming) {
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// When operated externally, the plugin does not bother with 'opening' and 'closing' status
|
|
289
|
+
// Open means magnetic sensor not detected, doesn't really mean the door is open
|
|
290
|
+
if (hasProperty(data, 'open')) {
|
|
291
|
+
const isOpen = data.open === 1
|
|
292
|
+
switch (this.cacheCurrent) {
|
|
293
|
+
case 0:
|
|
294
|
+
case 2: {
|
|
295
|
+
// Homebridge has garage as open or opening
|
|
296
|
+
if (isOpen) {
|
|
297
|
+
// Meross has reported open
|
|
298
|
+
// Nothing to do
|
|
299
|
+
} else {
|
|
300
|
+
// Meross has reported closed
|
|
301
|
+
this.service.updateCharacteristic(this.hapChar.TargetDoorState, 1)
|
|
302
|
+
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 1)
|
|
303
|
+
this.accessory.eveService.addEntry({ status: 1 })
|
|
304
|
+
this.cacheCurrent = 1
|
|
305
|
+
this.cacheTarget = 1
|
|
306
|
+
this.accessory.log(`${platformLang.curTarg} [${this.states[this.cacheTarget]}]`)
|
|
307
|
+
this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
|
|
308
|
+
}
|
|
309
|
+
break
|
|
310
|
+
}
|
|
311
|
+
case 1: {
|
|
312
|
+
// Homebridge has garage as closed
|
|
313
|
+
if (isOpen) {
|
|
314
|
+
// Meross has reported open
|
|
315
|
+
this.service.updateCharacteristic(this.hapChar.TargetDoorState, 0)
|
|
316
|
+
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 0)
|
|
317
|
+
this.accessory.eveService.addEntry({ status: 0 })
|
|
318
|
+
this.service.updateCharacteristic(
|
|
319
|
+
this.eveChar.TimesOpened,
|
|
320
|
+
this.service.getCharacteristic(this.eveChar.TimesOpened).value + 1,
|
|
321
|
+
)
|
|
322
|
+
const initialTime = this.accessory.eveService.getInitialTime()
|
|
323
|
+
this.service.updateCharacteristic(
|
|
324
|
+
this.eveChar.LastActivation,
|
|
325
|
+
Math.round(new Date().valueOf() / 1000) - initialTime,
|
|
326
|
+
)
|
|
327
|
+
this.cacheCurrent = 0
|
|
328
|
+
this.cacheTarget = 0
|
|
329
|
+
this.accessory.log(`${platformLang.curTarg} [${this.states[this.cacheTarget]}]`)
|
|
330
|
+
this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
|
|
331
|
+
} else {
|
|
332
|
+
// Meross has reported closed
|
|
333
|
+
// Nothing to do
|
|
334
|
+
}
|
|
335
|
+
break
|
|
336
|
+
}
|
|
337
|
+
case 3: {
|
|
338
|
+
// Homebridge has garage as closing
|
|
339
|
+
if (isOpen) {
|
|
340
|
+
// Meross has reported open
|
|
341
|
+
this.service.updateCharacteristic(this.hapChar.TargetDoorState, 0)
|
|
342
|
+
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 0)
|
|
343
|
+
this.accessory.eveService.addEntry({ status: 0 })
|
|
344
|
+
this.service.updateCharacteristic(
|
|
345
|
+
this.eveChar.TimesOpened,
|
|
346
|
+
this.service.getCharacteristic(this.eveChar.TimesOpened).value + 1,
|
|
347
|
+
)
|
|
348
|
+
const initialTime = this.accessory.eveService.getInitialTime()
|
|
349
|
+
this.service.updateCharacteristic(
|
|
350
|
+
this.eveChar.LastActivation,
|
|
351
|
+
Math.round(new Date().valueOf() / 1000) - initialTime,
|
|
352
|
+
)
|
|
353
|
+
this.cacheCurrent = 0
|
|
354
|
+
this.cacheTarget = 0
|
|
355
|
+
this.accessory.log(`${platformLang.curTarg} [${this.states[this.cacheTarget]}]`)
|
|
356
|
+
this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
|
|
357
|
+
} else {
|
|
358
|
+
// Meross has reported closed
|
|
359
|
+
this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 1)
|
|
360
|
+
this.accessory.eveService.addEntry({ status: 1 })
|
|
361
|
+
this.cacheCurrent = 1
|
|
362
|
+
this.accessory.log(`${platformLang.curState} [${this.states[this.cacheCurrent]}]`)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Cancel any 'extreme' polling intervals from setting the garage to close
|
|
366
|
+
if (this.extremePolling) {
|
|
367
|
+
clearInterval(this.extremePolling)
|
|
368
|
+
this.extremePolling = false
|
|
369
|
+
}
|
|
370
|
+
break
|
|
371
|
+
}
|
|
372
|
+
default:
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|