@homebridge-plugins/homebridge-ecovacs 7.0.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 +570 -0
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/config.schema.json +952 -0
- package/lib/homebridge-ui/public/index.html +297 -0
- package/lib/homebridge-ui/server.js +10 -0
- package/lib/index.js +8 -0
- package/lib/platform.js +1557 -0
- package/lib/utils/constants.js +102 -0
- package/lib/utils/custom-chars.js +50 -0
- package/lib/utils/functions.js +18 -0
- package/lib/utils/lang-en.js +79 -0
- package/package.json +75 -0
package/lib/platform.js
ADDED
|
@@ -0,0 +1,1557 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
|
|
5
|
+
import { countries, EcoVacsAPI } from 'ecovacs-deebot'
|
|
6
|
+
|
|
7
|
+
import platformConsts from './utils/constants.js'
|
|
8
|
+
import platformChars from './utils/custom-chars.js'
|
|
9
|
+
import { parseError, sleep } from './utils/functions.js'
|
|
10
|
+
import platformLang from './utils/lang-en.js'
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url)
|
|
13
|
+
const plugin = require('../package.json')
|
|
14
|
+
|
|
15
|
+
const devicesInHB = new Map()
|
|
16
|
+
|
|
17
|
+
export default class {
|
|
18
|
+
constructor(log, config, api) {
|
|
19
|
+
if (!log || !api) {
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Begin plugin initialisation
|
|
24
|
+
try {
|
|
25
|
+
this.api = api
|
|
26
|
+
this.log = log
|
|
27
|
+
this.isBeta = plugin.version.includes('beta')
|
|
28
|
+
|
|
29
|
+
// Configuration objects for accessories
|
|
30
|
+
this.deviceConf = {}
|
|
31
|
+
this.ignoredDevices = []
|
|
32
|
+
|
|
33
|
+
// Make sure user is running Homebridge v1.6 or above
|
|
34
|
+
if (!api?.versionGreaterOrEqual('1.6.0')) {
|
|
35
|
+
throw new Error(platformLang.hbVersionFail)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check the user has configured the plugin
|
|
39
|
+
if (!config) {
|
|
40
|
+
throw new Error(platformLang.pluginNotConf)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Log some environment info for debugging
|
|
44
|
+
this.log(
|
|
45
|
+
'%s v%s | System %s | Node %s | HB v%s | HAPNodeJS v%s...',
|
|
46
|
+
platformLang.initialising,
|
|
47
|
+
plugin.version,
|
|
48
|
+
process.platform,
|
|
49
|
+
process.version,
|
|
50
|
+
api.serverVersion,
|
|
51
|
+
api.hap.HAPLibraryVersion(),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
// Check the user has entered the required config fields
|
|
55
|
+
if (!config.username || !config.password || !config.countryCode) {
|
|
56
|
+
throw new Error(platformLang.missingCreds)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Apply the user's configuration
|
|
60
|
+
this.config = platformConsts.defaultConfig
|
|
61
|
+
this.applyUserConfig(config)
|
|
62
|
+
|
|
63
|
+
// Create further variables needed by the plugin
|
|
64
|
+
this.hapErr = api.hap.HapStatusError
|
|
65
|
+
this.hapChar = api.hap.Characteristic
|
|
66
|
+
this.hapServ = api.hap.Service
|
|
67
|
+
|
|
68
|
+
// Set up the Homebridge events
|
|
69
|
+
this.api.on('didFinishLaunching', () => this.pluginSetup())
|
|
70
|
+
this.api.on('shutdown', () => this.pluginShutdown())
|
|
71
|
+
} catch (err) {
|
|
72
|
+
// Catch any errors during initialisation
|
|
73
|
+
log.warn('***** %s. *****', platformLang.disabling)
|
|
74
|
+
log.warn('***** %s. *****', parseError(err, [
|
|
75
|
+
platformLang.hbVersionFail,
|
|
76
|
+
platformLang.pluginNotConf,
|
|
77
|
+
platformLang.missingCreds,
|
|
78
|
+
platformLang.invalidCCode,
|
|
79
|
+
platformLang.invalidPassword,
|
|
80
|
+
platformLang.invalidUsername,
|
|
81
|
+
]))
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
applyUserConfig(config) {
|
|
86
|
+
// These shorthand functions save line space during config parsing
|
|
87
|
+
const logDefault = (k, def) => {
|
|
88
|
+
this.log.warn('%s [%s] %s %s.', platformLang.cfgItem, k, platformLang.cfgDef, def)
|
|
89
|
+
}
|
|
90
|
+
const logDuplicate = (k) => {
|
|
91
|
+
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgDup)
|
|
92
|
+
}
|
|
93
|
+
const logIgnore = (k) => {
|
|
94
|
+
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgIgn)
|
|
95
|
+
}
|
|
96
|
+
const logIgnoreItem = (k) => {
|
|
97
|
+
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgIgnItem)
|
|
98
|
+
}
|
|
99
|
+
const logIncrease = (k, min) => {
|
|
100
|
+
this.log.warn('%s [%s] %s %s.', platformLang.cfgItem, k, platformLang.cfgLow, min)
|
|
101
|
+
}
|
|
102
|
+
const logQuotes = (k) => {
|
|
103
|
+
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgQts)
|
|
104
|
+
}
|
|
105
|
+
const logRemove = (k) => {
|
|
106
|
+
this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgRmv)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Begin applying the user's config
|
|
110
|
+
Object.entries(config).forEach((entry) => {
|
|
111
|
+
const [key, val] = entry
|
|
112
|
+
switch (key) {
|
|
113
|
+
case 'countryCode':
|
|
114
|
+
if (typeof val !== 'string' || val === '') {
|
|
115
|
+
throw new Error(platformLang.invalidCCode)
|
|
116
|
+
}
|
|
117
|
+
this.config.countryCode = val.toUpperCase().replace(/[^A-Z]+/g, '')
|
|
118
|
+
if (!Object.keys(countries).includes(this.config.countryCode)) {
|
|
119
|
+
throw new Error(platformLang.invalidCCode)
|
|
120
|
+
}
|
|
121
|
+
break
|
|
122
|
+
case 'devices':
|
|
123
|
+
if (Array.isArray(val) && val.length > 0) {
|
|
124
|
+
val.forEach((x) => {
|
|
125
|
+
if (!x.deviceId) {
|
|
126
|
+
logIgnoreItem(key)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
const id = x.deviceId.replace(/\s+/g, '')
|
|
130
|
+
if (Object.keys(this.deviceConf).includes(id)) {
|
|
131
|
+
logDuplicate(`${key}.${id}`)
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
const entries = Object.entries(x)
|
|
135
|
+
if (entries.length === 1) {
|
|
136
|
+
logRemove(`${key}.${id}`)
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
this.deviceConf[id] = platformConsts.defaultDevice
|
|
140
|
+
entries.forEach((subEntry) => {
|
|
141
|
+
const [k, v] = subEntry
|
|
142
|
+
switch (k) {
|
|
143
|
+
case 'areaNote1':
|
|
144
|
+
case 'areaNote2':
|
|
145
|
+
case 'areaNote3':
|
|
146
|
+
case 'areaNote4':
|
|
147
|
+
case 'areaNote5':
|
|
148
|
+
case 'areaNote6':
|
|
149
|
+
case 'areaNote7':
|
|
150
|
+
case 'areaNote8':
|
|
151
|
+
case 'areaNote9':
|
|
152
|
+
case 'areaNote10':
|
|
153
|
+
case 'areaNote11':
|
|
154
|
+
case 'areaNote12':
|
|
155
|
+
case 'areaNote13':
|
|
156
|
+
case 'areaNote14':
|
|
157
|
+
case 'areaNote15':
|
|
158
|
+
// Just ignore the command notes as they are intended for user information during configuration only.
|
|
159
|
+
break
|
|
160
|
+
case 'areaType1':
|
|
161
|
+
case 'areaType2':
|
|
162
|
+
case 'areaType3':
|
|
163
|
+
case 'areaType4':
|
|
164
|
+
case 'areaType5':
|
|
165
|
+
case 'areaType6':
|
|
166
|
+
case 'areaType7':
|
|
167
|
+
case 'areaType8':
|
|
168
|
+
case 'areaType9':
|
|
169
|
+
case 'areaType10':
|
|
170
|
+
case 'areaType11':
|
|
171
|
+
case 'areaType12':
|
|
172
|
+
case 'areaType13':
|
|
173
|
+
case 'areaType14':
|
|
174
|
+
case 'areaType15':
|
|
175
|
+
if (typeof v !== 'string' || v === '') {
|
|
176
|
+
logIgnore(`${key}.${id}.${k}`)
|
|
177
|
+
} else {
|
|
178
|
+
// Just take over the command type as it comes from a string enumeration.
|
|
179
|
+
this.deviceConf[id][k] = v
|
|
180
|
+
}
|
|
181
|
+
break
|
|
182
|
+
case 'customAreaCoordinates1':
|
|
183
|
+
case 'customAreaCoordinates2':
|
|
184
|
+
case 'customAreaCoordinates3':
|
|
185
|
+
case 'customAreaCoordinates4':
|
|
186
|
+
case 'customAreaCoordinates5':
|
|
187
|
+
case 'customAreaCoordinates6':
|
|
188
|
+
case 'customAreaCoordinates7':
|
|
189
|
+
case 'customAreaCoordinates8':
|
|
190
|
+
case 'customAreaCoordinates9':
|
|
191
|
+
case 'customAreaCoordinates10':
|
|
192
|
+
case 'customAreaCoordinates11':
|
|
193
|
+
case 'customAreaCoordinates12':
|
|
194
|
+
case 'customAreaCoordinates13':
|
|
195
|
+
case 'customAreaCoordinates14':
|
|
196
|
+
case 'customAreaCoordinates15': {
|
|
197
|
+
if (typeof v !== 'string' || v === '') {
|
|
198
|
+
logIgnore(`${key}.${id}.${k}`)
|
|
199
|
+
} else {
|
|
200
|
+
// Strip off everything else than signs, figures, periods and commas.
|
|
201
|
+
const stripped = v.replace(/[^-\d.,]+/g, '')
|
|
202
|
+
if (stripped) {
|
|
203
|
+
this.deviceConf[id][k] = stripped
|
|
204
|
+
} else {
|
|
205
|
+
logIgnore(`${key}.${id}.${k}`)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
break
|
|
209
|
+
}
|
|
210
|
+
case 'deviceId':
|
|
211
|
+
case 'label':
|
|
212
|
+
break
|
|
213
|
+
case 'hideMotionSensor':
|
|
214
|
+
case 'showBattHumidity':
|
|
215
|
+
case 'showMotionLowBatt':
|
|
216
|
+
case 'supportTrueDetect':
|
|
217
|
+
if (typeof v === 'string') {
|
|
218
|
+
logQuotes(`${key}.${id}.${k}`)
|
|
219
|
+
}
|
|
220
|
+
this.deviceConf[id][k] = v === 'false' ? false : !!v
|
|
221
|
+
break
|
|
222
|
+
case 'ignoreDevice':
|
|
223
|
+
if (typeof v === 'string') {
|
|
224
|
+
logQuotes(`${key}.${id}.${k}`)
|
|
225
|
+
}
|
|
226
|
+
if (!!v && v !== 'false') {
|
|
227
|
+
this.ignoredDevices.push(id)
|
|
228
|
+
}
|
|
229
|
+
break
|
|
230
|
+
case 'lowBattThreshold':
|
|
231
|
+
case 'motionDuration': {
|
|
232
|
+
if (typeof v === 'string') {
|
|
233
|
+
logQuotes(`${key}.${id}.${k}`)
|
|
234
|
+
}
|
|
235
|
+
const intVal = Number.parseInt(v, 10)
|
|
236
|
+
if (Number.isNaN(intVal)) {
|
|
237
|
+
logDefault(`${key}.${id}.${k}`, platformConsts.defaultValues[k])
|
|
238
|
+
this.deviceConf[id][k] = platformConsts.defaultValues[k]
|
|
239
|
+
} else if (intVal < platformConsts.minValues[k]) {
|
|
240
|
+
logIncrease(`${key}.${id}.${k}`, platformConsts.minValues[k])
|
|
241
|
+
this.deviceConf[id][k] = platformConsts.minValues[k]
|
|
242
|
+
} else {
|
|
243
|
+
this.deviceConf[id][k] = intVal
|
|
244
|
+
}
|
|
245
|
+
break
|
|
246
|
+
}
|
|
247
|
+
case 'pollInterval': {
|
|
248
|
+
if (typeof v === 'string') {
|
|
249
|
+
logQuotes(`${key}.${id}.${k}`)
|
|
250
|
+
}
|
|
251
|
+
const intVal = Number.parseInt(v, 10)
|
|
252
|
+
if (Number.isNaN(intVal)) {
|
|
253
|
+
logDefault(`${key}.${id}.${k}`, platformConsts.defaultValues[k])
|
|
254
|
+
this.deviceConf[id][k] = platformConsts.defaultValues[k]
|
|
255
|
+
} else if (intVal === 0) {
|
|
256
|
+
this.deviceConf[id][k] = intVal
|
|
257
|
+
} else if (intVal < platformConsts.minValues[k]) {
|
|
258
|
+
logIncrease(key, platformConsts.minValues[k])
|
|
259
|
+
this.deviceConf[id][k] = platformConsts.minValues[k]
|
|
260
|
+
} else {
|
|
261
|
+
this.deviceConf[id][k] = intVal
|
|
262
|
+
}
|
|
263
|
+
break
|
|
264
|
+
}
|
|
265
|
+
case 'showAirDryingSwitch': {
|
|
266
|
+
const inSet = platformConsts.allowed[k].includes(v)
|
|
267
|
+
if (typeof v !== 'string' || !inSet) {
|
|
268
|
+
logIgnore(`${key}.${id}.${k}`)
|
|
269
|
+
} else {
|
|
270
|
+
this.deviceConf[id][k] = inSet ? v : platformConsts.defaultValues[k]
|
|
271
|
+
}
|
|
272
|
+
break
|
|
273
|
+
}
|
|
274
|
+
case 'spotAreaIDs1':
|
|
275
|
+
case 'spotAreaIDs2':
|
|
276
|
+
case 'spotAreaIDs3':
|
|
277
|
+
case 'spotAreaIDs4':
|
|
278
|
+
case 'spotAreaIDs5':
|
|
279
|
+
case 'spotAreaIDs6':
|
|
280
|
+
case 'spotAreaIDs7':
|
|
281
|
+
case 'spotAreaIDs8':
|
|
282
|
+
case 'spotAreaIDs9':
|
|
283
|
+
case 'spotAreaIDs10':
|
|
284
|
+
case 'spotAreaIDs11':
|
|
285
|
+
case 'spotAreaIDs12':
|
|
286
|
+
case 'spotAreaIDs13':
|
|
287
|
+
case 'spotAreaIDs14':
|
|
288
|
+
case 'spotAreaIDs15': {
|
|
289
|
+
if (typeof v !== 'string' || v === '') {
|
|
290
|
+
logIgnore(`${key}.${id}.${k}`)
|
|
291
|
+
} else {
|
|
292
|
+
// Strip off everything else than figures and commas.
|
|
293
|
+
const stripped = v.replace(/[^\d,]+/g, '')
|
|
294
|
+
if (stripped) {
|
|
295
|
+
this.deviceConf[id][k] = stripped
|
|
296
|
+
} else {
|
|
297
|
+
logIgnore(`${key}.${id}.${k}`)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
break
|
|
301
|
+
}
|
|
302
|
+
default:
|
|
303
|
+
logRemove(`${key}.${id}.${k}`)
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
} else {
|
|
308
|
+
logIgnore(key)
|
|
309
|
+
}
|
|
310
|
+
break
|
|
311
|
+
case 'disableDeviceLogging':
|
|
312
|
+
case 'useYeedi':
|
|
313
|
+
if (typeof val === 'string') {
|
|
314
|
+
logQuotes(key)
|
|
315
|
+
}
|
|
316
|
+
this.config[key] = val === 'false' ? false : !!val
|
|
317
|
+
break
|
|
318
|
+
case 'name':
|
|
319
|
+
case 'platform':
|
|
320
|
+
break
|
|
321
|
+
case 'password':
|
|
322
|
+
if (typeof val !== 'string' || val === '') {
|
|
323
|
+
throw new Error(platformLang.invalidPassword)
|
|
324
|
+
}
|
|
325
|
+
this.config.password = val
|
|
326
|
+
break
|
|
327
|
+
case 'username':
|
|
328
|
+
if (typeof val !== 'string' || val === '') {
|
|
329
|
+
throw new Error(platformLang.invalidUsername)
|
|
330
|
+
}
|
|
331
|
+
this.config.username = val.replace(/\s+/g, '')
|
|
332
|
+
break
|
|
333
|
+
default:
|
|
334
|
+
logRemove(key)
|
|
335
|
+
break
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async pluginSetup() {
|
|
341
|
+
// Plugin has finished initialising so now onto setup
|
|
342
|
+
try {
|
|
343
|
+
// Log that the plugin initialisation has been successful
|
|
344
|
+
this.log('%s.', platformLang.initialised)
|
|
345
|
+
|
|
346
|
+
// Sort out some logging functions
|
|
347
|
+
if (this.isBeta) {
|
|
348
|
+
this.log.debug = this.log
|
|
349
|
+
this.log.debugWarn = this.log.warn
|
|
350
|
+
|
|
351
|
+
// Log that using a beta will generate a lot of debug logs
|
|
352
|
+
if (this.isBeta) {
|
|
353
|
+
const divide = '*'.repeat(platformLang.beta.length + 1) // don't forget the full stop (+1!)
|
|
354
|
+
this.log.warn(divide)
|
|
355
|
+
this.log.warn(`${platformLang.beta}.`)
|
|
356
|
+
this.log.warn(divide)
|
|
357
|
+
}
|
|
358
|
+
} else {
|
|
359
|
+
this.log.debug = () => {}
|
|
360
|
+
this.log.debugWarn = () => {}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Require any libraries that the accessory instances use
|
|
364
|
+
this.cusChar = new platformChars(this.api)
|
|
365
|
+
|
|
366
|
+
// Connect to ECOVACS/Yeedi
|
|
367
|
+
this.ecovacsAPI = new EcoVacsAPI(
|
|
368
|
+
EcoVacsAPI.getDeviceId(this.api.hap.uuid.generate(this.config.username)),
|
|
369
|
+
this.config.countryCode,
|
|
370
|
+
countries[this.config.countryCode].continent,
|
|
371
|
+
this.config.useYeedi ? 'yeedi.com' : 'ecovacs.com',
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
// Display version of the ecovacs-deebot library in the log
|
|
375
|
+
this.log('%s v%s.', platformLang.ecovacsLibVersion, this.ecovacsAPI.getVersion())
|
|
376
|
+
|
|
377
|
+
// Attempt to log in to ECOVACS/Yeedi
|
|
378
|
+
try {
|
|
379
|
+
await this.ecovacsAPI.connect(this.config.username, EcoVacsAPI.md5(this.config.password))
|
|
380
|
+
} catch (err) {
|
|
381
|
+
// Check if password error and reattempt with base64 decoded version of password
|
|
382
|
+
if (err.message?.includes('1010')) {
|
|
383
|
+
this.config.password = Buffer.from(this.config.password, 'base64')
|
|
384
|
+
.toString('utf8')
|
|
385
|
+
.replace(/\r\n|\n|\r/g, '')
|
|
386
|
+
.trim()
|
|
387
|
+
await this.ecovacsAPI.connect(
|
|
388
|
+
this.config.username,
|
|
389
|
+
EcoVacsAPI.md5(this.config.password),
|
|
390
|
+
)
|
|
391
|
+
} else {
|
|
392
|
+
throw err
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Get a device list from ECOVACS/Yeedi
|
|
397
|
+
const deviceList = await this.ecovacsAPI.devices()
|
|
398
|
+
|
|
399
|
+
// Check the request for device list was successful
|
|
400
|
+
if (!Array.isArray(deviceList)) {
|
|
401
|
+
throw new TypeError(platformLang.deviceListFail)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Initialise each device into Homebridge
|
|
405
|
+
this.log('[%s] %s.', deviceList.length, platformLang.deviceCount(this.config.useYeedi ? 'Yeedi' : 'ECOVACS'))
|
|
406
|
+
for (let i = 0; i < deviceList.length; i += 1) {
|
|
407
|
+
await this.initialiseDevice(deviceList[i])
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Start the polling intervals for device state refresh, each device may have a different refresh time
|
|
411
|
+
// We add a refresh interval per device later in initialiseDevice()
|
|
412
|
+
this.refreshIntervals = {}
|
|
413
|
+
|
|
414
|
+
// Setup successful
|
|
415
|
+
this.log('%s. %s', platformLang.complete, platformLang.welcome)
|
|
416
|
+
} catch (err) {
|
|
417
|
+
// Catch any errors during setup
|
|
418
|
+
this.log.warn('***** %s. *****', platformLang.disabling)
|
|
419
|
+
this.log.warn('***** %s. *****', parseError(err, [platformLang.deviceListFail]))
|
|
420
|
+
this.pluginShutdown()
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
pluginShutdown() {
|
|
425
|
+
// A function that is called when the plugin fails to load or Homebridge restarts
|
|
426
|
+
try {
|
|
427
|
+
// Stop the refresh intervals
|
|
428
|
+
Object.keys(this.refreshIntervals).forEach((id) => {
|
|
429
|
+
clearInterval(this.refreshIntervals[id])
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
// Disconnect from each ECOVACS/Yeedi device
|
|
433
|
+
devicesInHB.forEach((accessory) => {
|
|
434
|
+
if (accessory.control?.is_ready) {
|
|
435
|
+
accessory.control.disconnect()
|
|
436
|
+
}
|
|
437
|
+
})
|
|
438
|
+
} catch (err) {
|
|
439
|
+
// No need to show errors at this point
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
initialiseDevice(device) {
|
|
444
|
+
try {
|
|
445
|
+
// Generate the Homebridge UUID from the device id
|
|
446
|
+
const uuid = this.api.hap.uuid.generate(device.did)
|
|
447
|
+
|
|
448
|
+
// If the accessory is in the ignored devices list then remove it
|
|
449
|
+
if (this.ignoredDevices.includes(device.did)) {
|
|
450
|
+
if (devicesInHB.has(uuid)) {
|
|
451
|
+
this.removeAccessory(devicesInHB.get(uuid))
|
|
452
|
+
}
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Load the device control information from ECOVACS/Yeedi
|
|
457
|
+
const loadedDevice = this.ecovacsAPI.getVacBot(
|
|
458
|
+
this.ecovacsAPI.uid,
|
|
459
|
+
EcoVacsAPI.REALM,
|
|
460
|
+
this.ecovacsAPI.resource,
|
|
461
|
+
this.ecovacsAPI.user_access_token,
|
|
462
|
+
device,
|
|
463
|
+
countries[this.config.countryCode].continent,
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
// Get the cached accessory or add to Homebridge if it doesn't exist
|
|
467
|
+
const accessory = devicesInHB.get(uuid) || this.addAccessory(loadedDevice)
|
|
468
|
+
|
|
469
|
+
accessory.context.rawConfig = this.deviceConf?.[device.did] || platformConsts.defaultDevice
|
|
470
|
+
|
|
471
|
+
// Final check the accessory now exists in Homebridge
|
|
472
|
+
if (!accessory) {
|
|
473
|
+
throw new Error(platformLang.accNotFound)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Sort out some logging functions per accessory
|
|
477
|
+
if (this.isBeta) {
|
|
478
|
+
accessory.log = msg => this.log('[%s] %s.', accessory.displayName, msg)
|
|
479
|
+
accessory.logWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
|
|
480
|
+
accessory.logDebug = msg => this.log('[%s] %s.', accessory.displayName, msg)
|
|
481
|
+
accessory.logDebugWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
|
|
482
|
+
} else {
|
|
483
|
+
if (this.config.disableDeviceLogging) {
|
|
484
|
+
accessory.log = () => {}
|
|
485
|
+
accessory.logWarn = () => {}
|
|
486
|
+
} else {
|
|
487
|
+
accessory.log = msg => this.log('[%s] %s.', accessory.displayName, msg)
|
|
488
|
+
accessory.logWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
|
|
489
|
+
}
|
|
490
|
+
accessory.logDebug = () => {}
|
|
491
|
+
accessory.logDebugWarn = () => {}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Initially set the device online value to false (to be updated later)
|
|
495
|
+
accessory.context.isOnline = false
|
|
496
|
+
accessory.context.lastMsg = ''
|
|
497
|
+
|
|
498
|
+
// Add the 'clean' switch service if it doesn't already exist
|
|
499
|
+
const cleanService = accessory.getService('Clean') || accessory.addService(this.hapServ.Switch, 'Clean', 'clean')
|
|
500
|
+
if (!cleanService.testCharacteristic(this.hapChar.ConfiguredName)) {
|
|
501
|
+
cleanService.addCharacteristic(this.hapChar.ConfiguredName)
|
|
502
|
+
cleanService.updateCharacteristic(this.hapChar.ConfiguredName, 'Clean')
|
|
503
|
+
}
|
|
504
|
+
if (!cleanService.testCharacteristic(this.hapChar.ServiceLabelIndex)) {
|
|
505
|
+
cleanService.addCharacteristic(this.hapChar.ServiceLabelIndex)
|
|
506
|
+
cleanService.updateCharacteristic(this.hapChar.ServiceLabelIndex, 1)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Add the 'charge' switch service if it doesn't already exist
|
|
510
|
+
const chargeService = accessory.getService('Go Charge') || accessory.addService(this.hapServ.Switch, 'Go Charge', 'gocharge')
|
|
511
|
+
if (!chargeService.testCharacteristic(this.hapChar.ConfiguredName)) {
|
|
512
|
+
chargeService.addCharacteristic(this.hapChar.ConfiguredName)
|
|
513
|
+
chargeService.updateCharacteristic(this.hapChar.ConfiguredName, 'Go Charge')
|
|
514
|
+
}
|
|
515
|
+
if (!chargeService.testCharacteristic(this.hapChar.ServiceLabelIndex)) {
|
|
516
|
+
chargeService.addCharacteristic(this.hapChar.ServiceLabelIndex)
|
|
517
|
+
chargeService.updateCharacteristic(this.hapChar.ServiceLabelIndex, 2)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Check if the speed characteristic has been added
|
|
521
|
+
if (!cleanService.testCharacteristic(this.cusChar.MaxSpeed)) {
|
|
522
|
+
cleanService.addCharacteristic(this.cusChar.MaxSpeed)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Add the Eve characteristic for custom commands if any exist
|
|
526
|
+
if (accessory.context.rawConfig.areaType1) {
|
|
527
|
+
if (!cleanService.testCharacteristic(this.cusChar.PredefinedArea)) {
|
|
528
|
+
cleanService.addCharacteristic(this.cusChar.PredefinedArea)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Add the set characteristic
|
|
532
|
+
cleanService
|
|
533
|
+
.getCharacteristic(this.cusChar.PredefinedArea)
|
|
534
|
+
.onSet(async value => this.internalPredefinedAreaUpdate(accessory, value))
|
|
535
|
+
} else if (cleanService.testCharacteristic(this.cusChar.PredefinedArea)) {
|
|
536
|
+
cleanService.removeCharacteristic(cleanService.getCharacteristic(this.cusChar.PredefinedArea))
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Add the set handler to the 'clean' switch on/off characteristic
|
|
540
|
+
cleanService
|
|
541
|
+
.getCharacteristic(this.hapChar.On)
|
|
542
|
+
.updateValue(accessory.context.cacheClean === 'auto')
|
|
543
|
+
.removeOnSet()
|
|
544
|
+
.onSet(async value => this.internalCleanUpdate(accessory, value))
|
|
545
|
+
|
|
546
|
+
// Add the set handler to the 'max speed' switch on/off characteristic
|
|
547
|
+
cleanService.getCharacteristic(this.cusChar.MaxSpeed)
|
|
548
|
+
.onSet(async value => this.internalSpeedUpdate(accessory, value))
|
|
549
|
+
|
|
550
|
+
// Add the set handler to the 'charge' switch on/off characteristic
|
|
551
|
+
chargeService
|
|
552
|
+
.getCharacteristic(this.hapChar.On)
|
|
553
|
+
.updateValue(accessory.context.cacheCharge === 'charging')
|
|
554
|
+
.removeOnSet()
|
|
555
|
+
.onSet(async value => this.internalChargeUpdate(accessory, value))
|
|
556
|
+
|
|
557
|
+
// Add the 'attention' motion service if it doesn't already exist
|
|
558
|
+
if (!accessory.getService('Attention') && !accessory.context.rawConfig.hideMotionSensor) {
|
|
559
|
+
accessory.addService(this.hapServ.MotionSensor, 'Attention', 'attention')
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Remove the 'attention' motion service if it exists and user doesn't want it
|
|
563
|
+
if (accessory.getService('Attention') && accessory.context.rawConfig.hideMotionSensor) {
|
|
564
|
+
accessory.removeService(accessory.getService('Attention'))
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Set the motion sensor off if exists when the plugin initially loads
|
|
568
|
+
if (accessory.getService('Attention')) {
|
|
569
|
+
accessory.getService('Attention').updateCharacteristic(this.hapChar.MotionDetected, false)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Add the battery service if it doesn't already exist
|
|
573
|
+
if (!accessory.getService(this.hapServ.Battery)) {
|
|
574
|
+
accessory.addService(this.hapServ.Battery)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Add the 'battery' humidity service if it doesn't already exist and user wants it
|
|
578
|
+
if (!accessory.getService('Battery Level') && accessory.context.rawConfig.showBattHumidity) {
|
|
579
|
+
accessory.addService(this.hapServ.HumiditySensor, 'Battery Level', 'batterylevel')
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Remove the 'battery' humidity service if it exists and user doesn't want it
|
|
583
|
+
if (accessory.getService('Battery Level') && !accessory.context.rawConfig.showBattHumidity) {
|
|
584
|
+
accessory.removeService(accessory.getService('Battery Level'))
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Add or remove the 'air drying' switch service according to the configuration (if it doesn't already exist) and add the set handler to the 'air drying' switch on/off characteristic
|
|
588
|
+
if (
|
|
589
|
+
accessory.context.rawConfig.showAirDryingSwitch === 'yes'
|
|
590
|
+
|| (accessory.context.rawConfig.showAirDryingSwitch === 'presetting' && loadedDevice.hasAirDrying())
|
|
591
|
+
) {
|
|
592
|
+
const dryingService = accessory.getService('Air Drying') || accessory.addService(this.hapServ.Switch, 'Air Drying', 'airdrying')
|
|
593
|
+
if (!dryingService.testCharacteristic(this.hapChar.ConfiguredName)) {
|
|
594
|
+
dryingService.addCharacteristic(this.hapChar.ConfiguredName)
|
|
595
|
+
dryingService.updateCharacteristic(this.hapChar.ConfiguredName, 'Air Drying')
|
|
596
|
+
}
|
|
597
|
+
if (!dryingService.testCharacteristic(this.hapChar.ServiceLabelIndex)) {
|
|
598
|
+
dryingService.addCharacteristic(this.hapChar.ServiceLabelIndex)
|
|
599
|
+
dryingService.updateCharacteristic(this.hapChar.ServiceLabelIndex, 3)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
dryingService
|
|
603
|
+
.getCharacteristic(this.hapChar.On)
|
|
604
|
+
.updateValue(accessory.context.cacheAirDrying === 'airdrying')
|
|
605
|
+
.removeOnSet()
|
|
606
|
+
.onSet(async value => this.internalAirDryingUpdate(accessory, value))
|
|
607
|
+
} else if (accessory.getService('Air Drying')) {
|
|
608
|
+
accessory.removeService(accessory.getService('Air Drying'))
|
|
609
|
+
accessory.logDebug('air drying service removed')
|
|
610
|
+
} else {
|
|
611
|
+
accessory.logDebug('no air drying available or not configured')
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// TrueDetect service
|
|
615
|
+
if (accessory.context.rawConfig.supportTrueDetect) {
|
|
616
|
+
// Custom Eve characteristic like MaxSpeed
|
|
617
|
+
if (!cleanService.testCharacteristic(this.cusChar.TrueDetect)) {
|
|
618
|
+
cleanService.addCharacteristic(this.cusChar.TrueDetect)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Add the set handler to the 'true detect' switch on/off characteristic
|
|
622
|
+
cleanService.getCharacteristic(this.cusChar.TrueDetect)
|
|
623
|
+
.onSet(async value => this.internalTrueDetectUpdate(accessory, value))
|
|
624
|
+
} else if (accessory.getService('TrueDetect')) {
|
|
625
|
+
// Remove TrueDetect service if exists
|
|
626
|
+
accessory.removeService(accessory.getService('TrueDetect'))
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Save the device control information to the accessory
|
|
630
|
+
accessory.control = loadedDevice
|
|
631
|
+
|
|
632
|
+
// Some models can use a V2 for supported commands
|
|
633
|
+
accessory.context.commandSuffix = accessory.control.is950type_V2()
|
|
634
|
+
? '_V2'
|
|
635
|
+
: ''
|
|
636
|
+
|
|
637
|
+
// Set up a listener for the device 'ready' event
|
|
638
|
+
accessory.control.on('ready', (event) => {
|
|
639
|
+
if (event) {
|
|
640
|
+
this.externalReadyUpdate(accessory)
|
|
641
|
+
}
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
// Set up a listener for the device 'CleanReport' event
|
|
645
|
+
accessory.control.on('CleanReport', (newVal) => {
|
|
646
|
+
this.externalCleanUpdate(accessory, newVal)
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
// Set up a listener for the device 'CurrentCustomAreaValues' event
|
|
650
|
+
accessory.control.on('CurrentCustomAreaValues', (newVal) => {
|
|
651
|
+
accessory.logDebug(`CurrentCustomAreaValues: ${JSON.stringify(newVal)}`)
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
// Set up a listener for the device 'CleanSpeed' event
|
|
655
|
+
accessory.control.on('CleanSpeed', (newVal) => {
|
|
656
|
+
this.externalSpeedUpdate(accessory, newVal)
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
// Set up a listener for the device 'BatteryInfo' event
|
|
660
|
+
accessory.control.on('BatteryInfo', async (newVal) => {
|
|
661
|
+
await this.externalBatteryUpdate(accessory, newVal)
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
// Set up a listener for the device 'AirDryingState' event
|
|
665
|
+
// Only if the service exists
|
|
666
|
+
if (accessory.getService('Air Drying')) {
|
|
667
|
+
accessory.control.on('AirDryingState', (newVal) => {
|
|
668
|
+
this.externalAirDryingUpdate(accessory, newVal)
|
|
669
|
+
})
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Set up a listener for the device 'ChargeState' event
|
|
673
|
+
accessory.control.on('ChargeState', (newVal) => {
|
|
674
|
+
this.externalChargeUpdate(accessory, newVal)
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
// Set up a listener for the device 'NetInfoIP' event
|
|
678
|
+
accessory.control.on('NetInfoIP', (newVal) => {
|
|
679
|
+
this.externalIPUpdate(accessory, newVal)
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
// Set up a listener for the device 'NetInfoMAC' event
|
|
683
|
+
accessory.control.on('NetInfoMAC', (newVal) => {
|
|
684
|
+
this.externalMacUpdate(accessory, newVal)
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
if (accessory.context.rawConfig.supportTrueDetect) {
|
|
688
|
+
// Set up a listener for the device 'TrueDetect' event
|
|
689
|
+
accessory.control.on('TrueDetect', (newVal) => {
|
|
690
|
+
this.externalTrueDetectUpdate(accessory, newVal)
|
|
691
|
+
})
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Set up a listener for the device 'message' event
|
|
695
|
+
accessory.control.on('message', async (msg) => {
|
|
696
|
+
await this.externalMessageUpdate(accessory, msg)
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
// Set up a listener for the device 'Error' event
|
|
700
|
+
accessory.control.on('Error', async (err) => {
|
|
701
|
+
if (err) {
|
|
702
|
+
await this.externalErrorUpdate(accessory, err)
|
|
703
|
+
}
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
// Set up listeners for map data if accessory debug logging is on
|
|
707
|
+
accessory.control.on('Maps', (maps) => {
|
|
708
|
+
if (maps) {
|
|
709
|
+
accessory.logDebug(`Maps: ${JSON.stringify(maps)}`)
|
|
710
|
+
Object.keys(maps.maps).forEach((key) => {
|
|
711
|
+
accessory.control.run('GetSpotAreas', maps.maps[key].mapID)
|
|
712
|
+
accessory.control.run('GetVirtualBoundaries', maps.maps[key].mapID)
|
|
713
|
+
})
|
|
714
|
+
}
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
accessory.control.on('MapSpotAreas', (spotAreas) => {
|
|
718
|
+
if (spotAreas) {
|
|
719
|
+
accessory.logDebug(`MapSpotAreas: ${JSON.stringify(spotAreas)}`)
|
|
720
|
+
Object.keys(spotAreas.mapSpotAreas).forEach((key) => {
|
|
721
|
+
accessory.control.run(
|
|
722
|
+
'GetSpotAreaInfo',
|
|
723
|
+
spotAreas.mapID,
|
|
724
|
+
spotAreas.mapSpotAreas[key].mapSpotAreaID,
|
|
725
|
+
)
|
|
726
|
+
})
|
|
727
|
+
}
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
accessory.control.on('MapSpotAreaInfo', (area) => {
|
|
731
|
+
if (area) {
|
|
732
|
+
accessory.logDebug(`MapSpotAreaInfo: ${JSON.stringify(area)}`)
|
|
733
|
+
}
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
accessory.control.on('MapVirtualBoundaries', (vbs) => {
|
|
737
|
+
if (vbs) {
|
|
738
|
+
accessory.logDebug(`MapVirtualBoundaries: ${JSON.stringify(vbs)}`)
|
|
739
|
+
const vbsCombined = [...vbs.mapVirtualWalls, ...vbs.mapNoMopZones]
|
|
740
|
+
const virtualBoundaryArray = []
|
|
741
|
+
Object.keys(vbsCombined).forEach((key) => {
|
|
742
|
+
virtualBoundaryArray[vbsCombined[key].mapVirtualBoundaryID] = vbsCombined[key]
|
|
743
|
+
})
|
|
744
|
+
Object.keys(virtualBoundaryArray).forEach((key) => {
|
|
745
|
+
accessory.control.run(
|
|
746
|
+
'GetVirtualBoundaryInfo',
|
|
747
|
+
vbs.mapID,
|
|
748
|
+
virtualBoundaryArray[key].mapVirtualBoundaryID,
|
|
749
|
+
virtualBoundaryArray[key].mapVirtualBoundaryType,
|
|
750
|
+
)
|
|
751
|
+
})
|
|
752
|
+
}
|
|
753
|
+
})
|
|
754
|
+
|
|
755
|
+
accessory.control.on('MapVirtualBoundaryInfo', (vb) => {
|
|
756
|
+
if (vb) {
|
|
757
|
+
accessory.logDebug(`MapVirtualBoundaryInfo: ${JSON.stringify(vb)}`)
|
|
758
|
+
}
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
// Connect to the device
|
|
762
|
+
accessory.control.connect()
|
|
763
|
+
|
|
764
|
+
// Refresh the current state of all the accessories
|
|
765
|
+
this.refreshAccessory(accessory)
|
|
766
|
+
const { pollInterval } = accessory.context.rawConfig[device] || platformConsts.defaultValues
|
|
767
|
+
if (pollInterval > 0) {
|
|
768
|
+
this.refreshIntervals[device.did] = setInterval(() => {
|
|
769
|
+
devicesInHB.get(this.api.hap.uuid.generate(device.did)).control?.refresh()
|
|
770
|
+
}, pollInterval * 1000)
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Update any changes to the accessory to the platform
|
|
774
|
+
this.api.updatePlatformAccessories([accessory])
|
|
775
|
+
devicesInHB.set(accessory.UUID, accessory)
|
|
776
|
+
|
|
777
|
+
// Log configuration and device initialisation
|
|
778
|
+
this.log(
|
|
779
|
+
'[%s] %s: %s.',
|
|
780
|
+
accessory.displayName,
|
|
781
|
+
platformLang.devInitOpts,
|
|
782
|
+
JSON.stringify(accessory.context.rawConfig),
|
|
783
|
+
)
|
|
784
|
+
this.log(
|
|
785
|
+
'[%s] %s [%s] %s %s.',
|
|
786
|
+
accessory.displayName,
|
|
787
|
+
platformLang.devInit,
|
|
788
|
+
device.did,
|
|
789
|
+
platformLang.addInfo,
|
|
790
|
+
JSON.stringify(device),
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
// If after five seconds the device hasn't responded then mark as offline
|
|
794
|
+
setTimeout(() => {
|
|
795
|
+
if (!accessory.context.isOnline) {
|
|
796
|
+
accessory.logWarn(platformLang.repOffline)
|
|
797
|
+
}
|
|
798
|
+
}, 5000)
|
|
799
|
+
} catch (err) {
|
|
800
|
+
const dName = device.nick || device.did
|
|
801
|
+
this.log.warn('[%s] %s %s.', dName, platformLang.devNotInit, parseError(err, [platformLang.accNotFound]))
|
|
802
|
+
this.log.warn(err)
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
refreshAccessory(accessory) {
|
|
807
|
+
try {
|
|
808
|
+
// Check the device has initialised already
|
|
809
|
+
if (!accessory.control) {
|
|
810
|
+
return
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Set up a flag to check later if we have had a response
|
|
814
|
+
accessory.context.hadResponse = false
|
|
815
|
+
|
|
816
|
+
// Run the commands to get the state of the device
|
|
817
|
+
accessory.logDebug(`${platformLang.sendCmd} [GetBatteryState]`)
|
|
818
|
+
accessory.control.run('GetBatteryState')
|
|
819
|
+
|
|
820
|
+
accessory.logDebug(`${platformLang.sendCmd} [GetChargeState]`)
|
|
821
|
+
accessory.control.run('GetChargeState')
|
|
822
|
+
|
|
823
|
+
if (accessory.getService('Air Drying')) {
|
|
824
|
+
accessory.logDebug(`${platformLang.sendCmd} [GetAirDrying]`)
|
|
825
|
+
accessory.control.run('GetAirDrying')
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
accessory.logDebug(`${platformLang.sendCmd} [GetCleanState${accessory.context.commandSuffix}]`)
|
|
829
|
+
accessory.control.run(`GetCleanState${accessory.context.commandSuffix}`)
|
|
830
|
+
|
|
831
|
+
accessory.logDebug(`${platformLang.sendCmd} [GetCleanSpeed]`)
|
|
832
|
+
accessory.control.run('GetCleanSpeed')
|
|
833
|
+
|
|
834
|
+
accessory.logDebug(`${platformLang.sendCmd} [GetNetInfo]`)
|
|
835
|
+
accessory.control.run('GetNetInfo')
|
|
836
|
+
|
|
837
|
+
// TrueDetect if the accessory supports it
|
|
838
|
+
if (accessory.context.rawConfig.supportTrueDetect) {
|
|
839
|
+
accessory.logDebug(`${platformLang.sendCmd} [GetTrueDetect]`)
|
|
840
|
+
accessory.control.run('GetTrueDetect')
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
setTimeout(() => {
|
|
844
|
+
if (!accessory.context.isOnline && accessory.context.hadResponse) {
|
|
845
|
+
accessory.logDebug(platformLang.repOnline)
|
|
846
|
+
accessory.context.isOnline = true
|
|
847
|
+
this.api.updatePlatformAccessories([accessory])
|
|
848
|
+
devicesInHB.set(accessory.UUID, accessory)
|
|
849
|
+
}
|
|
850
|
+
if (accessory.context.isOnline && !accessory.context.hadResponse) {
|
|
851
|
+
accessory.logDebug(platformLang.repOffline)
|
|
852
|
+
accessory.context.isOnline = false
|
|
853
|
+
this.api.updatePlatformAccessories([accessory])
|
|
854
|
+
devicesInHB.set(accessory.UUID, accessory)
|
|
855
|
+
}
|
|
856
|
+
}, 5000)
|
|
857
|
+
} catch (err) {
|
|
858
|
+
// Catch any errors in the refresh process
|
|
859
|
+
accessory.logWarn(`${platformLang.devNotRef} ${parseError(err)}`)
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
addAccessory(device) {
|
|
864
|
+
// Add an accessory to Homebridge
|
|
865
|
+
let displayName = 'Unknown'
|
|
866
|
+
try {
|
|
867
|
+
displayName = device.vacuum.nick || device.vacuum.did
|
|
868
|
+
const accessory = new this.api.platformAccessory(
|
|
869
|
+
displayName,
|
|
870
|
+
this.api.hap.uuid.generate(device.vacuum.did),
|
|
871
|
+
)
|
|
872
|
+
accessory
|
|
873
|
+
.getService(this.hapServ.AccessoryInformation)
|
|
874
|
+
.setCharacteristic(this.hapChar.Name, displayName)
|
|
875
|
+
.setCharacteristic(this.hapChar.ConfiguredName, displayName)
|
|
876
|
+
.setCharacteristic(this.hapChar.SerialNumber, device.vacuum.did)
|
|
877
|
+
.setCharacteristic(this.hapChar.Manufacturer, device.vacuum.company)
|
|
878
|
+
.setCharacteristic(this.hapChar.Model, device.deviceModel)
|
|
879
|
+
.setCharacteristic(this.hapChar.Identify, true)
|
|
880
|
+
|
|
881
|
+
// Add context information for Homebridge plugin-ui
|
|
882
|
+
accessory.context.ecoDeviceId = device.vacuum.did
|
|
883
|
+
accessory.context.ecoCompany = device.vacuum.company
|
|
884
|
+
accessory.context.ecoModel = device.deviceModel
|
|
885
|
+
accessory.context.ecoClass = device.vacuum.class
|
|
886
|
+
accessory.context.ecoResource = device.vacuum.resource
|
|
887
|
+
accessory.context.ecoImage = device.deviceImageURL
|
|
888
|
+
this.api.registerPlatformAccessories(plugin.name, plugin.alias, [accessory])
|
|
889
|
+
devicesInHB.set(accessory.UUID, accessory)
|
|
890
|
+
this.log('[%s] %s.', displayName, platformLang.devAdd)
|
|
891
|
+
return accessory
|
|
892
|
+
} catch (err) {
|
|
893
|
+
// Catch any errors during add
|
|
894
|
+
this.log.warn('[%s] %s %s.', displayName, platformLang.devNotAdd, parseError(err))
|
|
895
|
+
return false
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
configureAccessory(accessory) {
|
|
900
|
+
// Add the configured accessory to our global map
|
|
901
|
+
devicesInHB.set(accessory.UUID, accessory)
|
|
902
|
+
accessory
|
|
903
|
+
.getService('Clean')
|
|
904
|
+
.getCharacteristic(this.api.hap.Characteristic.On)
|
|
905
|
+
.onSet(() => {
|
|
906
|
+
this.log.warn('[%s] %s.', accessory.displayName, platformLang.accNotReady)
|
|
907
|
+
throw new this.api.hap.HapStatusError(-70402)
|
|
908
|
+
})
|
|
909
|
+
.updateValue(new this.api.hap.HapStatusError(-70402))
|
|
910
|
+
accessory
|
|
911
|
+
.getService('Go Charge')
|
|
912
|
+
.getCharacteristic(this.api.hap.Characteristic.On)
|
|
913
|
+
.onSet(() => {
|
|
914
|
+
this.log.warn('[%s] %s.', accessory.displayName, platformLang.accNotReady)
|
|
915
|
+
throw new this.api.hap.HapStatusError(-70402)
|
|
916
|
+
})
|
|
917
|
+
.updateValue(new this.api.hap.HapStatusError(-70402))
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
removeAccessory(accessory) {
|
|
921
|
+
// Remove an accessory from Homebridge
|
|
922
|
+
try {
|
|
923
|
+
this.api.unregisterPlatformAccessories(plugin.name, plugin.alias, [accessory])
|
|
924
|
+
devicesInHB.delete(accessory.UUID)
|
|
925
|
+
this.log('[%s] %s.', accessory.displayName, platformLang.devRemove)
|
|
926
|
+
} catch (err) {
|
|
927
|
+
// Catch any errors during remove
|
|
928
|
+
this.log.warn('[%s] %s %s.', accessory.displayName, platformLang.devNotRemove, parseError(err))
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
async internalCleanUpdate(accessory, value) {
|
|
933
|
+
try {
|
|
934
|
+
// Don't continue if we can't send commands to the device
|
|
935
|
+
if (!accessory.control) {
|
|
936
|
+
throw new Error(platformLang.errNotInit)
|
|
937
|
+
}
|
|
938
|
+
if (!accessory.control.is_ready) {
|
|
939
|
+
throw new Error(platformLang.errNotReady)
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// A one-second delay seems to make turning off the 'charge' switch more responsive
|
|
943
|
+
await sleep(1)
|
|
944
|
+
|
|
945
|
+
// Turn the 'charge' switch off since we have commanded the 'clean' switch
|
|
946
|
+
accessory.getService('Go Charge').updateCharacteristic(this.hapChar.On, false)
|
|
947
|
+
|
|
948
|
+
// Select the correct command to run, either start or stop cleaning
|
|
949
|
+
const order = value ? `Clean${accessory.context.commandSuffix}` : 'Stop'
|
|
950
|
+
|
|
951
|
+
// Log the update
|
|
952
|
+
accessory.log(`${platformLang.curCleaning} [${value ? platformLang.cleaning : platformLang.stop}}]`)
|
|
953
|
+
|
|
954
|
+
// Send the command
|
|
955
|
+
accessory.logDebug(`${platformLang.sendCmd} [${order}]`)
|
|
956
|
+
accessory.control.run(order)
|
|
957
|
+
} catch (err) {
|
|
958
|
+
// Catch any errors during the process
|
|
959
|
+
accessory.logWarn(`${platformLang.cleanFail} ${parseError(err, [platformLang.errNotInit, platformLang.errNotReady])}`)
|
|
960
|
+
|
|
961
|
+
// Throw a 'no response' error and set a timeout to revert this after 2 seconds
|
|
962
|
+
setTimeout(() => {
|
|
963
|
+
accessory
|
|
964
|
+
.getService('Clean')
|
|
965
|
+
.updateCharacteristic(this.hapChar.On, accessory.context.cacheClean === 'auto')
|
|
966
|
+
}, 2000)
|
|
967
|
+
throw new this.hapErr(-70402)
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
async internalSpeedUpdate(accessory, value) {
|
|
972
|
+
try {
|
|
973
|
+
// Don't continue if we can't send commands to the device
|
|
974
|
+
if (!accessory.control) {
|
|
975
|
+
throw new Error(platformLang.errNotInit)
|
|
976
|
+
}
|
|
977
|
+
if (!accessory.control.is_ready) {
|
|
978
|
+
throw new Error(platformLang.errNotReady)
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Set speed to max (3) if value is true otherwise set to standard (2)
|
|
982
|
+
const command = value ? 3 : 2
|
|
983
|
+
|
|
984
|
+
// Log the update
|
|
985
|
+
accessory.log(`${platformLang.curSpeed} [${platformConsts.speed2Label[command]}]`)
|
|
986
|
+
|
|
987
|
+
// Send the command
|
|
988
|
+
accessory.logDebug(`${platformLang.sendCmd} [SetCleanSpeed: ${command}]`)
|
|
989
|
+
accessory.control.run('SetCleanSpeed', command)
|
|
990
|
+
} catch (err) {
|
|
991
|
+
// Catch any errors during the process
|
|
992
|
+
accessory.logWarn(`${platformLang.speedFail} ${parseError(err, [platformLang.errNotInit, platformLang.errNotReady])}`)
|
|
993
|
+
|
|
994
|
+
// Throw a 'no response' error and set a timeout to revert this after 2 seconds
|
|
995
|
+
setTimeout(() => {
|
|
996
|
+
accessory
|
|
997
|
+
.getService('Clean')
|
|
998
|
+
.updateCharacteristic(this.cusChar.MaxSpeed, [3, 4].includes(accessory.context.cacheSpeed))
|
|
999
|
+
}, 2000)
|
|
1000
|
+
throw new this.hapErr(-70402)
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
async internalPredefinedAreaUpdate(accessory, value) {
|
|
1005
|
+
try {
|
|
1006
|
+
// Don't continue if we can't send commands to the device
|
|
1007
|
+
if (!accessory.control) {
|
|
1008
|
+
throw new Error(platformLang.errNotInit)
|
|
1009
|
+
}
|
|
1010
|
+
if (!accessory.control.is_ready) {
|
|
1011
|
+
throw new Error(platformLang.errNotReady)
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Eve app for some reason still sends values with decimal places
|
|
1015
|
+
value = Math.round(value)
|
|
1016
|
+
|
|
1017
|
+
// A value of 0 doesn't do anything
|
|
1018
|
+
if (value === 0) {
|
|
1019
|
+
accessory.logDebugWarn(platformLang.returningAsValueNull)
|
|
1020
|
+
return
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Avoid quick switching with this function
|
|
1024
|
+
const updateKey = Math.random()
|
|
1025
|
+
.toString(36)
|
|
1026
|
+
.substr(2, 8)
|
|
1027
|
+
accessory.context.lastCommandKey = updateKey
|
|
1028
|
+
await sleep(1)
|
|
1029
|
+
if (updateKey !== accessory.context.lastCommandKey) {
|
|
1030
|
+
accessory.logWarn(platformLang.skippingValue)
|
|
1031
|
+
return
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Obtain the area type from the device config
|
|
1035
|
+
const areaType = accessory.context.rawConfig[`areaType${value}`]
|
|
1036
|
+
|
|
1037
|
+
// Don't continue if no command type for this number has been configured
|
|
1038
|
+
if (!areaType) {
|
|
1039
|
+
throw new Error(`${platformLang.noTypeForArea}: ${value}`)
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
accessory.log(`${platformLang.typeForArea} ${value}: ${areaType}`)
|
|
1043
|
+
|
|
1044
|
+
// Obtain the command from the device config
|
|
1045
|
+
const command = accessory.context.rawConfig[areaType === 'spotArea'
|
|
1046
|
+
? `spotAreaIDs${value}`
|
|
1047
|
+
: `customAreaCoordinates${value}`]
|
|
1048
|
+
|
|
1049
|
+
// Don't continue if no command for this number has been configured
|
|
1050
|
+
if (!command) {
|
|
1051
|
+
throw new Error(`${platformLang.noCommandForArea}: ${value}`)
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
accessory.log(`${platformLang.commandForArea} ${value}: ${command}`)
|
|
1055
|
+
|
|
1056
|
+
// Send the command
|
|
1057
|
+
switch (areaType) {
|
|
1058
|
+
case 'spotArea':
|
|
1059
|
+
accessory.logDebug(`${platformLang.sendCmd} [SpotArea${accessory.context.commandSuffix}: ${command}]`)
|
|
1060
|
+
|
|
1061
|
+
if (accessory.context.commandSuffix === '_V2') {
|
|
1062
|
+
accessory.control.run('SpotArea_V2', command)
|
|
1063
|
+
} else {
|
|
1064
|
+
accessory.control.run('SpotArea', 'start', command)
|
|
1065
|
+
}
|
|
1066
|
+
break
|
|
1067
|
+
|
|
1068
|
+
case 'customArea':
|
|
1069
|
+
accessory.logDebug(`${platformLang.sendCmd} [CustomArea${accessory.context.commandSuffix}: ${command}]`)
|
|
1070
|
+
|
|
1071
|
+
if (accessory.context.commandSuffix === '_V2') {
|
|
1072
|
+
accessory.control.run('CustomArea_V2', command)
|
|
1073
|
+
} else {
|
|
1074
|
+
accessory.control.run('CustomArea', 'start', command)
|
|
1075
|
+
}
|
|
1076
|
+
break
|
|
1077
|
+
|
|
1078
|
+
default:
|
|
1079
|
+
throw new Error(`${areaType}: ${platformLang.unknownCommandTypeForArea}`)
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
accessory.log(platformLang.commandSent)
|
|
1083
|
+
|
|
1084
|
+
// Set the value back to 0 after two seconds and turn the main ON switch on
|
|
1085
|
+
setTimeout(() => {
|
|
1086
|
+
accessory.getService('Clean').updateCharacteristic(this.cusChar.PredefinedArea, 0)
|
|
1087
|
+
accessory.getService('Clean').updateCharacteristic(this.hapChar.On, true)
|
|
1088
|
+
accessory.log(platformLang.characteristicsReset)
|
|
1089
|
+
}, 2000)
|
|
1090
|
+
} catch (err) {
|
|
1091
|
+
// Catch any errors during the process
|
|
1092
|
+
accessory.logWarn(`${platformLang.speedFail} ${parseError(err, [platformLang.errNotInit, platformLang.errNotReady])}`)
|
|
1093
|
+
|
|
1094
|
+
// Throw a 'no response' error and set a timeout to revert this after 2 seconds
|
|
1095
|
+
setTimeout(() => {
|
|
1096
|
+
accessory.getService('Clean').updateCharacteristic(this.cusChar.PredefinedArea, 0)
|
|
1097
|
+
}, 2000)
|
|
1098
|
+
throw new this.hapErr(-70402)
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
async internalChargeUpdate(accessory, value) {
|
|
1103
|
+
try {
|
|
1104
|
+
// Don't continue if we can't send commands to the device
|
|
1105
|
+
if (!accessory.control) {
|
|
1106
|
+
throw new Error(platformLang.errNotInit)
|
|
1107
|
+
}
|
|
1108
|
+
if (!accessory.control.is_ready) {
|
|
1109
|
+
throw new Error(platformLang.errNotReady)
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// A one-second delay seems to make everything more responsive
|
|
1113
|
+
await sleep(1)
|
|
1114
|
+
|
|
1115
|
+
// Don't continue if the device is already charging
|
|
1116
|
+
const battService = accessory.getService(this.hapServ.Battery)
|
|
1117
|
+
if (battService.getCharacteristic(this.hapChar.ChargingState).value !== 0) {
|
|
1118
|
+
return
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Select the correct command to run, either start or stop going to charge
|
|
1122
|
+
const order = value ? 'Charge' : 'Stop'
|
|
1123
|
+
|
|
1124
|
+
// Log the update
|
|
1125
|
+
accessory.log(`${platformLang.curCharging} [${value ? platformLang.returning : platformLang.stop}]`)
|
|
1126
|
+
|
|
1127
|
+
// Send the command
|
|
1128
|
+
accessory.logDebug(`${platformLang.sendCmd} [${order}]`)
|
|
1129
|
+
accessory.control.run(order)
|
|
1130
|
+
} catch (err) {
|
|
1131
|
+
// Catch any errors during the process
|
|
1132
|
+
accessory.logWarn(`${platformLang.chargeFail} ${parseError(err, [platformLang.errNotInit, platformLang.errNotReady])}`)
|
|
1133
|
+
|
|
1134
|
+
// Throw a 'no response' error and set a timeout to revert this after 2 seconds
|
|
1135
|
+
setTimeout(() => {
|
|
1136
|
+
accessory
|
|
1137
|
+
.getService('Go Charge')
|
|
1138
|
+
.updateCharacteristic(this.hapChar.On, accessory.context.cacheCharge === 'charging')
|
|
1139
|
+
}, 2000)
|
|
1140
|
+
throw new this.hapErr(-70402)
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
async internalAirDryingUpdate(accessory, value) {
|
|
1145
|
+
try {
|
|
1146
|
+
// Don't continue if we can't send commands to the device
|
|
1147
|
+
if (!accessory.control) {
|
|
1148
|
+
throw new Error(platformLang.errNotInit)
|
|
1149
|
+
}
|
|
1150
|
+
if (!accessory.control.is_ready) {
|
|
1151
|
+
throw new Error(platformLang.errNotReady)
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// A one-second delay seems to make everything more responsive
|
|
1155
|
+
await sleep(1)
|
|
1156
|
+
|
|
1157
|
+
// Select the correct command to run, either start or stop air drying.
|
|
1158
|
+
const order = value ? 'AirDryingStart' : 'AirDryingStop'
|
|
1159
|
+
|
|
1160
|
+
// Send the command
|
|
1161
|
+
accessory.logDebug(`${platformLang.sendCmd} [${order}]`)
|
|
1162
|
+
accessory.control.run(order)
|
|
1163
|
+
} catch (err) {
|
|
1164
|
+
// Catch any errors during the process
|
|
1165
|
+
accessory.logWarn(`${platformLang.airDryingFail} ${parseError(err, [platformLang.errNotInit, platformLang.errNotReady])}`)
|
|
1166
|
+
|
|
1167
|
+
// Throw a 'no response' error and set a timeout to revert this after 2 seconds
|
|
1168
|
+
setTimeout(() => {
|
|
1169
|
+
accessory
|
|
1170
|
+
.getService('Air Drying')
|
|
1171
|
+
.updateCharacteristic(this.hapChar.On, accessory.context.cacheAirDrying === 'airdrying')
|
|
1172
|
+
}, 2000)
|
|
1173
|
+
throw new this.hapErr(-70402)
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
async internalTrueDetectUpdate(accessory, value) {
|
|
1178
|
+
try {
|
|
1179
|
+
// Don't continue if we can't send commands to the device
|
|
1180
|
+
if (!accessory.control) {
|
|
1181
|
+
throw new Error(platformLang.errNotInit)
|
|
1182
|
+
}
|
|
1183
|
+
if (!accessory.control.is_ready) {
|
|
1184
|
+
throw new Error(platformLang.errNotReady)
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Select the correct command to run, either enable or disable TrueDetect.
|
|
1188
|
+
const command = value ? 'EnableTrueDetect' : 'DisableTrueDetect'
|
|
1189
|
+
|
|
1190
|
+
// Log the update
|
|
1191
|
+
accessory.log(`${platformLang.curTrueDetect} [${value ? 'yes' : 'no'}]`)
|
|
1192
|
+
|
|
1193
|
+
// Send the command
|
|
1194
|
+
accessory.logDebug(`${platformLang.sendCmd} [${command}]`)
|
|
1195
|
+
accessory.control.run(command)
|
|
1196
|
+
} catch (err) {
|
|
1197
|
+
// Catch any errors during the process
|
|
1198
|
+
accessory.logWarn(`${platformLang.cleanFail} ${parseError(err, [platformLang.errNotInit, platformLang.errNotReady])}`)
|
|
1199
|
+
|
|
1200
|
+
// Throw a 'no response' error and set a timeout to revert this after 2 seconds
|
|
1201
|
+
setTimeout(() => {
|
|
1202
|
+
accessory
|
|
1203
|
+
.getService('Clean')
|
|
1204
|
+
.updateCharacteristic(this.cusChar.TrueDetect, false)
|
|
1205
|
+
}, 2000)
|
|
1206
|
+
throw new this.hapErr(-70402)
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
externalReadyUpdate(accessory) {
|
|
1211
|
+
try {
|
|
1212
|
+
// Called on the 'ready' event sent by the device so request update for states
|
|
1213
|
+
accessory.logDebug(`${platformLang.sendCmd} [GetBatteryState]`)
|
|
1214
|
+
accessory.control.run('GetBatteryState')
|
|
1215
|
+
|
|
1216
|
+
accessory.log(`${platformLang.sendCmd} [GetChargeState]`)
|
|
1217
|
+
accessory.control.run('GetChargeState')
|
|
1218
|
+
|
|
1219
|
+
accessory.logDebug(`${platformLang.sendCmd} [GetCleanState${accessory.context.commandSuffix}]`)
|
|
1220
|
+
accessory.control.run(`GetCleanState${accessory.context.commandSuffix}`)
|
|
1221
|
+
|
|
1222
|
+
accessory.logDebug(`${platformLang.sendCmd} [GetCleanSpeed]`)
|
|
1223
|
+
accessory.control.run('GetCleanSpeed')
|
|
1224
|
+
|
|
1225
|
+
accessory.logDebug(`${platformLang.sendCmd} [GetNetInfo]`)
|
|
1226
|
+
accessory.control.run('GetNetInfo')
|
|
1227
|
+
|
|
1228
|
+
accessory.logDebug(`${platformLang.sendCmd} [GetMaps]`)
|
|
1229
|
+
accessory.control.run('GetMaps')
|
|
1230
|
+
} catch (err) {
|
|
1231
|
+
// Catch any errors during the process
|
|
1232
|
+
accessory.logWarn(`${platformLang.inRdyFail} ${parseError(err)}`)
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
externalCleanUpdate(accessory, newVal) {
|
|
1237
|
+
try {
|
|
1238
|
+
// Log the received update
|
|
1239
|
+
accessory.logDebug(`${platformLang.receiveCmd} [CleanReport: ${newVal}]`)
|
|
1240
|
+
|
|
1241
|
+
// Check if the new cleaning state is different from the cached state
|
|
1242
|
+
if (accessory.context.cacheClean !== newVal) {
|
|
1243
|
+
// State is different so update service
|
|
1244
|
+
accessory
|
|
1245
|
+
.getService('Clean')
|
|
1246
|
+
.updateCharacteristic(
|
|
1247
|
+
this.hapChar.On,
|
|
1248
|
+
['auto', 'clean', 'edge', 'spot', 'spotarea', 'customarea'].includes(
|
|
1249
|
+
newVal.toLowerCase().replace(/[^a-z]+/g, ''),
|
|
1250
|
+
),
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
// Log the change
|
|
1254
|
+
accessory.log(`${platformLang.curCleaning} [${newVal}]`)
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// Always update the cache with the new cleaning status
|
|
1258
|
+
accessory.context.cacheClean = newVal
|
|
1259
|
+
} catch (err) {
|
|
1260
|
+
// Catch any errors during the process
|
|
1261
|
+
accessory.logWarn(`${platformLang.inClnFail} ${parseError(err)}`)
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
externalSpeedUpdate(accessory, newVal) {
|
|
1266
|
+
try {
|
|
1267
|
+
// Log the received update
|
|
1268
|
+
accessory.logDebug(`${platformLang.receiveCmd} [CleanSpeed: ${newVal}]`)
|
|
1269
|
+
|
|
1270
|
+
// Check if the new cleaning state is different from the cached state
|
|
1271
|
+
if (accessory.context.cacheSpeed !== newVal) {
|
|
1272
|
+
// State is different so update service
|
|
1273
|
+
accessory
|
|
1274
|
+
.getService('Clean')
|
|
1275
|
+
.updateCharacteristic(this.cusChar.MaxSpeed, [3, 4].includes(newVal))
|
|
1276
|
+
|
|
1277
|
+
// Log the change
|
|
1278
|
+
accessory.log(`${platformLang.curSpeed} [${platformConsts.speed2Label[newVal]}]`)
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Always update the cache with the new speed status
|
|
1282
|
+
accessory.context.cacheSpeed = newVal
|
|
1283
|
+
} catch (err) {
|
|
1284
|
+
// Catch any errors during the process
|
|
1285
|
+
accessory.logWarn(`${platformLang.inSpdFail} ${parseError(err)}`)
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
externalAirDryingUpdate(accessory, newVal) {
|
|
1290
|
+
try {
|
|
1291
|
+
// Log the received update
|
|
1292
|
+
accessory.logDebug(`${platformLang.receiveCmd} [AirDryingState: ${newVal}]`)
|
|
1293
|
+
|
|
1294
|
+
// Check if the new drying state is different from the cached state
|
|
1295
|
+
if (accessory.context.cacheAirDrying !== newVal) {
|
|
1296
|
+
// State is different so update service
|
|
1297
|
+
accessory
|
|
1298
|
+
.getService('Air Drying')
|
|
1299
|
+
.updateCharacteristic(this.hapChar.On, newVal === 'airdrying')
|
|
1300
|
+
|
|
1301
|
+
// Log the change
|
|
1302
|
+
accessory.log(`${platformLang.curAirDrying} [${newVal}]`)
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// Always update the cache with the new drying status
|
|
1306
|
+
accessory.context.cacheAirDrying = newVal
|
|
1307
|
+
} catch (err) {
|
|
1308
|
+
// Catch any errors during the process
|
|
1309
|
+
accessory.logWarn(`${platformLang.inAirFail} ${parseError(err)}`)
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
externalTrueDetectUpdate(accessory, newVal) {
|
|
1314
|
+
try {
|
|
1315
|
+
// Log the received update
|
|
1316
|
+
accessory.logDebug(`${platformLang.receiveCmd} [TrueDetect: ${newVal}]`)
|
|
1317
|
+
|
|
1318
|
+
// Check if the new charging state is different from the cached state
|
|
1319
|
+
if (accessory.context.trueDetect !== newVal) {
|
|
1320
|
+
// State is different so update service
|
|
1321
|
+
accessory
|
|
1322
|
+
.getService('Clean')
|
|
1323
|
+
.updateCharacteristic(this.cusChar.TrueDetect, newVal === 1)
|
|
1324
|
+
|
|
1325
|
+
// Log the change
|
|
1326
|
+
accessory.log(`${platformLang.curTrueDetect} [${newVal === 1 ? 'enabled' : 'disabled'}]`)
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// Always update the cache with the new charging status
|
|
1330
|
+
accessory.context.trueDetect = newVal
|
|
1331
|
+
} catch (err) {
|
|
1332
|
+
// Catch any errors during the process
|
|
1333
|
+
accessory.logWarn(`${platformLang.inTrDFail} ${parseError(err)}`)
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
externalChargeUpdate(accessory, newVal) {
|
|
1338
|
+
try {
|
|
1339
|
+
// Log the received update
|
|
1340
|
+
accessory.logDebug(`${platformLang.receiveCmd} [ChargeState: ${newVal}]`)
|
|
1341
|
+
|
|
1342
|
+
// Check if the new charging state is different from the cached state
|
|
1343
|
+
if (accessory.context.cacheCharge !== newVal) {
|
|
1344
|
+
// State is different so update service
|
|
1345
|
+
accessory
|
|
1346
|
+
.getService('Go Charge')
|
|
1347
|
+
.updateCharacteristic(this.hapChar.On, newVal === 'returning')
|
|
1348
|
+
const chargeState = newVal === 'charging' ? 1 : 0
|
|
1349
|
+
accessory
|
|
1350
|
+
.getService(this.hapServ.Battery)
|
|
1351
|
+
.updateCharacteristic(this.hapChar.ChargingState, chargeState)
|
|
1352
|
+
|
|
1353
|
+
// Log the change
|
|
1354
|
+
accessory.log(`${platformLang.curCharging} [${newVal}]`)
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// Always update the cache with the new charging status
|
|
1358
|
+
accessory.context.cacheCharge = newVal
|
|
1359
|
+
} catch (err) {
|
|
1360
|
+
// Catch any errors during the process
|
|
1361
|
+
accessory.logWarn(`${platformLang.inChgFail} ${parseError(err)}`)
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
externalIPUpdate(accessory, newVal) {
|
|
1366
|
+
try {
|
|
1367
|
+
// Log the received update
|
|
1368
|
+
accessory.logDebug(`${platformLang.receiveCmd} [NetInfoIP: ${newVal}]`)
|
|
1369
|
+
|
|
1370
|
+
// Check if the new IP is different from the cached IP
|
|
1371
|
+
if (accessory.context.ipAddress !== newVal) {
|
|
1372
|
+
// IP is different so update context info
|
|
1373
|
+
accessory.context.ipAddress = newVal
|
|
1374
|
+
|
|
1375
|
+
// Update the changes to the accessory to the platform
|
|
1376
|
+
this.api.updatePlatformAccessories([accessory])
|
|
1377
|
+
devicesInHB.set(accessory.UUID, accessory)
|
|
1378
|
+
}
|
|
1379
|
+
} catch (err) {
|
|
1380
|
+
// Catch any errors during the process
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
externalMacUpdate(accessory, newVal) {
|
|
1385
|
+
try {
|
|
1386
|
+
// Log the received update
|
|
1387
|
+
accessory.logDebug(`${platformLang.receiveCmd} [NetInfoMAC: ${newVal}]`)
|
|
1388
|
+
|
|
1389
|
+
// Check if the new MAC is different from the cached MAC
|
|
1390
|
+
if (accessory.context.macAddress !== newVal) {
|
|
1391
|
+
// MAC is different so update context info
|
|
1392
|
+
accessory.context.macAddress = newVal
|
|
1393
|
+
|
|
1394
|
+
// Update the changes to the accessory to the platform
|
|
1395
|
+
this.api.updatePlatformAccessories([accessory])
|
|
1396
|
+
devicesInHB.set(accessory.UUID, accessory)
|
|
1397
|
+
}
|
|
1398
|
+
} catch (err) {
|
|
1399
|
+
// Catch any errors during the process
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
async externalBatteryUpdate(accessory, newVal) {
|
|
1404
|
+
try {
|
|
1405
|
+
// Mark the device as online if it was offline before
|
|
1406
|
+
accessory.context.hadResponse = true
|
|
1407
|
+
|
|
1408
|
+
// Log the received update
|
|
1409
|
+
accessory.logDebug(`${platformLang.receiveCmd} [BatteryInfo: ${newVal}]`)
|
|
1410
|
+
|
|
1411
|
+
// Check the value given is between 0 and 100
|
|
1412
|
+
newVal = Math.min(Math.max(Math.round(newVal), 0), 100)
|
|
1413
|
+
|
|
1414
|
+
// Check if the new battery value is different from the cached state
|
|
1415
|
+
if (accessory.context.cacheBattery !== newVal) {
|
|
1416
|
+
// Value is different so update services
|
|
1417
|
+
const threshold = accessory.context.rawConfig.lowBattThreshold
|
|
1418
|
+
const lowBattStatus = newVal <= threshold ? 1 : 0
|
|
1419
|
+
accessory
|
|
1420
|
+
.getService(this.hapServ.Battery)
|
|
1421
|
+
.updateCharacteristic(this.hapChar.BatteryLevel, newVal)
|
|
1422
|
+
accessory
|
|
1423
|
+
.getService(this.hapServ.Battery)
|
|
1424
|
+
.updateCharacteristic(this.hapChar.StatusLowBattery, lowBattStatus)
|
|
1425
|
+
|
|
1426
|
+
// Also update the 'battery' humidity service if it exists
|
|
1427
|
+
if (accessory.context.rawConfig.showBattHumidity) {
|
|
1428
|
+
accessory
|
|
1429
|
+
.getService('Battery Level')
|
|
1430
|
+
.updateCharacteristic(this.hapChar.CurrentRelativeHumidity, newVal)
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// Log the change
|
|
1434
|
+
accessory.log(`${platformLang.curBatt} [${newVal}%]`)
|
|
1435
|
+
|
|
1436
|
+
// If the user wants a message and a buzz from the motion sensor then do it
|
|
1437
|
+
if (
|
|
1438
|
+
accessory.context.rawConfig.showMotionLowBatt
|
|
1439
|
+
&& newVal <= accessory.context.rawConfig.lowBattThreshold
|
|
1440
|
+
&& !accessory.cacheShownMotionLowBatt
|
|
1441
|
+
) {
|
|
1442
|
+
await this.externalMessageUpdate(accessory, `${platformLang.lowBattMsg + newVal}%`)
|
|
1443
|
+
accessory.cacheShownMotionLowBatt = true
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// Revert the cache to false once the device has charged above the threshold
|
|
1447
|
+
if (newVal > accessory.context.rawConfig.lowBattThreshold) {
|
|
1448
|
+
accessory.cacheShownMotionLowBatt = false
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// Always update the cache with the new battery value
|
|
1453
|
+
accessory.context.cacheBattery = newVal
|
|
1454
|
+
} catch (err) {
|
|
1455
|
+
// Catch any errors during the process
|
|
1456
|
+
accessory.logWarn(`${platformLang.inBattFail} ${parseError(err)}`)
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
async externalMessageUpdate(accessory, msg) {
|
|
1461
|
+
try {
|
|
1462
|
+
// Don't bother logging the same message as before
|
|
1463
|
+
if (accessory.context.lastMsg === msg) {
|
|
1464
|
+
return
|
|
1465
|
+
}
|
|
1466
|
+
accessory.context.lastMsg = msg
|
|
1467
|
+
|
|
1468
|
+
// Check if it's a no error message
|
|
1469
|
+
if (msg === 'NoError: Robot is operational') {
|
|
1470
|
+
return
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// Check to see if the motion sensor is already in use
|
|
1474
|
+
if (accessory.cacheInUse) {
|
|
1475
|
+
return
|
|
1476
|
+
}
|
|
1477
|
+
accessory.cacheInUse = true
|
|
1478
|
+
|
|
1479
|
+
// Log the message sent from the device
|
|
1480
|
+
accessory.log(`${platformLang.sentMsg} [${msg}]`)
|
|
1481
|
+
|
|
1482
|
+
// Update the motion sensor to motion detected if it exists
|
|
1483
|
+
if (accessory.getService('Attention')) {
|
|
1484
|
+
accessory.getService('Attention').updateCharacteristic(this.hapChar.MotionDetected, true)
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// The motion sensor stays on for the time configured by the user, so we wait
|
|
1488
|
+
setTimeout(() => {
|
|
1489
|
+
// Reset the motion sensor after waiting for the time above if it exists
|
|
1490
|
+
if (accessory.getService('Attention')) {
|
|
1491
|
+
accessory.getService('Attention').updateCharacteristic(this.hapChar.MotionDetected, false)
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// Update the inUse cache to false as we are complete here
|
|
1495
|
+
accessory.cacheInUse = false
|
|
1496
|
+
}, accessory.context.rawConfig.motionDuration * 1000)
|
|
1497
|
+
} catch (err) {
|
|
1498
|
+
// Catch any errors in the process
|
|
1499
|
+
accessory.logWarn(`${platformLang.inMsgFail} ${parseError(err)}`)
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
async externalErrorUpdate(accessory, err) {
|
|
1504
|
+
try {
|
|
1505
|
+
// Check if it's an offline notification but device was online
|
|
1506
|
+
if (err === 'Recipient unavailable' && accessory.context.isOnline) {
|
|
1507
|
+
accessory.log(platformLang.repOffline)
|
|
1508
|
+
accessory.context.isOnline = false
|
|
1509
|
+
this.api.updatePlatformAccessories([accessory])
|
|
1510
|
+
devicesInHB.set(accessory.UUID, accessory)
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// Check if it's a no error message
|
|
1514
|
+
if (err === 'NoError: Robot is operational') {
|
|
1515
|
+
return
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// Don't bother logging the same message as before
|
|
1519
|
+
if (accessory.context.lastMsg === err) {
|
|
1520
|
+
return
|
|
1521
|
+
}
|
|
1522
|
+
accessory.context.lastMsg = err
|
|
1523
|
+
|
|
1524
|
+
// Log the message sent from the device
|
|
1525
|
+
accessory.logWarn(`${platformLang.sentErr} [${err}]`)
|
|
1526
|
+
|
|
1527
|
+
// Check to see if the motion sensor is already in use
|
|
1528
|
+
if (accessory.cacheInUse) {
|
|
1529
|
+
return
|
|
1530
|
+
}
|
|
1531
|
+
accessory.cacheInUse = true
|
|
1532
|
+
|
|
1533
|
+
// Update the motion sensor to motion detected if it exists
|
|
1534
|
+
if (accessory.getService('Attention')) {
|
|
1535
|
+
accessory.getService('Attention').updateCharacteristic(this.hapChar.MotionDetected, true)
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// The device has an error so turn both 'clean' and 'charge' switches off
|
|
1539
|
+
accessory.getService('Clean').updateCharacteristic(this.hapChar.On, false)
|
|
1540
|
+
accessory.getService('Go Charge').updateCharacteristic(this.hapChar.On, false)
|
|
1541
|
+
|
|
1542
|
+
// The motion sensor stays on for the time configured by the user, so we wait
|
|
1543
|
+
setTimeout(() => {
|
|
1544
|
+
// Reset the motion sensor after waiting for the time above if it exists
|
|
1545
|
+
if (accessory.getService('Attention')) {
|
|
1546
|
+
accessory.getService('Attention').updateCharacteristic(this.hapChar.MotionDetected, false)
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// Update the inUse cache to false as we are complete here
|
|
1550
|
+
accessory.cacheInUse = false
|
|
1551
|
+
}, accessory.context.rawConfig.motionDuration * 1000)
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
// Catch any errors in the process
|
|
1554
|
+
accessory.logWarn(`${platformLang.inErrFail} ${parseError(error)}`)
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|