@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +1346 -0
  2. package/LICENSE +21 -0
  3. package/README.md +68 -0
  4. package/config.schema.json +2066 -0
  5. package/eslint.config.js +49 -0
  6. package/lib/connection/http.js +345 -0
  7. package/lib/connection/mqtt.js +174 -0
  8. package/lib/device/baby.js +532 -0
  9. package/lib/device/cooler-single.js +447 -0
  10. package/lib/device/diffuser.js +730 -0
  11. package/lib/device/fan.js +530 -0
  12. package/lib/device/garage-main.js +225 -0
  13. package/lib/device/garage-single.js +495 -0
  14. package/lib/device/garage-sub.js +376 -0
  15. package/lib/device/heater-single.js +445 -0
  16. package/lib/device/hub-contact.js +56 -0
  17. package/lib/device/hub-leak.js +86 -0
  18. package/lib/device/hub-main.js +403 -0
  19. package/lib/device/hub-sensor.js +115 -0
  20. package/lib/device/hub-smoke.js +40 -0
  21. package/lib/device/hub-valve.js +377 -0
  22. package/lib/device/humidifier.js +521 -0
  23. package/lib/device/index.js +63 -0
  24. package/lib/device/light-cct.js +474 -0
  25. package/lib/device/light-dimmer.js +312 -0
  26. package/lib/device/light-rgb.js +528 -0
  27. package/lib/device/outlet-multi.js +383 -0
  28. package/lib/device/outlet-single.js +405 -0
  29. package/lib/device/power-strip.js +282 -0
  30. package/lib/device/purifier-single.js +372 -0
  31. package/lib/device/purifier.js +403 -0
  32. package/lib/device/roller-location.js +317 -0
  33. package/lib/device/roller.js +234 -0
  34. package/lib/device/sensor-presence.js +201 -0
  35. package/lib/device/switch-multi.js +403 -0
  36. package/lib/device/switch-single.js +371 -0
  37. package/lib/device/template.js +177 -0
  38. package/lib/device/thermostat.js +493 -0
  39. package/lib/fakegato/LICENSE +21 -0
  40. package/lib/fakegato/fakegato-history.js +814 -0
  41. package/lib/fakegato/fakegato-storage.js +108 -0
  42. package/lib/fakegato/fakegato-timer.js +125 -0
  43. package/lib/fakegato/uuid.js +27 -0
  44. package/lib/homebridge-ui/public/index.html +316 -0
  45. package/lib/homebridge-ui/server.js +10 -0
  46. package/lib/index.js +8 -0
  47. package/lib/platform.js +1256 -0
  48. package/lib/utils/colour.js +581 -0
  49. package/lib/utils/constants.js +377 -0
  50. package/lib/utils/custom-chars.js +165 -0
  51. package/lib/utils/eve-chars.js +130 -0
  52. package/lib/utils/functions.js +39 -0
  53. package/lib/utils/lang-en.js +114 -0
  54. package/package.json +70 -0
@@ -0,0 +1,1256 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { existsSync, mkdirSync } from 'node:fs'
3
+ import { createRequire } from 'node:module'
4
+ import { join } from 'node:path'
5
+ import process from 'node:process'
6
+
7
+ import axios from 'axios'
8
+ import storage from 'node-persist'
9
+
10
+ import httpClient from './connection/http.js'
11
+ import deviceTypes from './device/index.js'
12
+ import eveService from './fakegato/fakegato-history.js'
13
+ import platformConsts from './utils/constants.js'
14
+ import platformChars from './utils/custom-chars.js'
15
+ import eveChars from './utils/eve-chars.js'
16
+ import { generateRandomString, hasProperty, parseError } from './utils/functions.js'
17
+ import platformLang from './utils/lang-en.js'
18
+
19
+ const require = createRequire(import.meta.url)
20
+ const plugin = require('../package.json')
21
+
22
+ export default class {
23
+ constructor(log, config, api) {
24
+ if (!log || !api) {
25
+ return
26
+ }
27
+
28
+ // Begin plugin initialisation
29
+ try {
30
+ this.api = api
31
+ this.log = log
32
+ this.isBeta = plugin.version.includes('beta')
33
+ this.cloudClient = false
34
+ this.deviceConf = {}
35
+ this.devicesInHB = new Map()
36
+ this.hideChannels = []
37
+ this.hideMasters = []
38
+ this.ignoredDevices = []
39
+ this.localUUIDs = []
40
+
41
+ // Make sure user is running Homebridge v1.4 or above
42
+ if (!api?.versionGreaterOrEqual('1.4.0')) {
43
+ throw new Error(platformLang.hbVersionFail)
44
+ }
45
+
46
+ // Check the user has configured the plugin
47
+ if (!config) {
48
+ throw new Error(platformLang.pluginNotConf)
49
+ }
50
+
51
+ // Log some environment info for debugging
52
+ this.log(
53
+ '%s v%s | System %s | Node %s | HB v%s | HAPNodeJS v%s...',
54
+ platformLang.initialising,
55
+ plugin.version,
56
+ process.platform,
57
+ process.version,
58
+ api.serverVersion,
59
+ api.hap.HAPLibraryVersion(),
60
+ )
61
+
62
+ // Apply the user's configuration
63
+ this.config = platformConsts.defaultConfig
64
+ this.applyUserConfig(config)
65
+
66
+ // Set up the Homebridge events
67
+ this.api.on('didFinishLaunching', () => this.pluginSetup())
68
+ this.api.on('shutdown', () => this.pluginShutdown())
69
+ } catch (err) {
70
+ // Catch any errors during initialisation
71
+ const eText = parseError(err, [platformLang.hbVersionFail, platformLang.pluginNotConf])
72
+ log.warn('***** %s. *****', platformLang.disabling)
73
+ log.warn('***** %s. *****', eText)
74
+ }
75
+ }
76
+
77
+ applyUserConfig(config) {
78
+ // These shorthand functions save line space during config parsing
79
+ const logDefault = (k, def) => {
80
+ this.log.warn('%s [%s] %s %s.', platformLang.cfgItem, k, platformLang.cfgDef, def)
81
+ }
82
+ const logDuplicate = (k) => {
83
+ this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgDup)
84
+ }
85
+ const logIgnore = (k) => {
86
+ this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgIgn)
87
+ }
88
+ const logIgnoreItem = (k) => {
89
+ this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgIgnItem)
90
+ }
91
+ const logIncrease = (k, min) => {
92
+ this.log.warn('%s [%s] %s %s.', platformLang.cfgItem, k, platformLang.cfgLow, min)
93
+ }
94
+ const logQuotes = (k) => {
95
+ this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgQts)
96
+ }
97
+ const logRemove = (k) => {
98
+ this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgRmv)
99
+ }
100
+
101
+ // Begin applying the user's config
102
+ Object.entries(config).forEach((entry) => {
103
+ const [key, val] = entry
104
+ switch (key) {
105
+ case 'babyDevices':
106
+ case 'diffuserDevices':
107
+ case 'fanDevices':
108
+ case 'garageDevices':
109
+ case 'humidifierDevices':
110
+ case 'lightDevices':
111
+ case 'multiDevices':
112
+ case 'purifierDevices':
113
+ case 'rollerDevices':
114
+ case 'sensorDevices':
115
+ case 'singleDevices':
116
+ case 'thermostatDevices':
117
+ if (Array.isArray(val) && val.length > 0) {
118
+ val.forEach((x) => {
119
+ if (
120
+ !(
121
+ x.serialNumber
122
+ && x.name
123
+ && (
124
+ (config.connection !== 'local' && x.connection !== 'local')
125
+ || (config.connection !== 'local' && x.connection === 'local' && x.deviceUrl)
126
+ || (config.connection === 'local' && x.model && x.deviceUrl)
127
+ )
128
+ )
129
+ ) {
130
+ logIgnoreItem(key)
131
+ return
132
+ }
133
+ const id = x.serialNumber.toLowerCase().replace(/[^a-z\d]+/g, '')
134
+ if (Object.keys(this.deviceConf).includes(id)) {
135
+ logDuplicate(`${key}.${id}`)
136
+ return
137
+ }
138
+ const entries = Object.entries(x)
139
+ if (entries.length < 3) {
140
+ logRemove(`${key}.${id}`)
141
+ return
142
+ }
143
+ this.deviceConf[id] = {}
144
+ entries.forEach((subEntry) => {
145
+ const [k, v] = subEntry
146
+ switch (k) {
147
+ case 'adaptiveLightingShift':
148
+ case 'brightnessStep':
149
+ case 'garageDoorOpeningTime':
150
+ case 'inUsePowerThreshold':
151
+ case 'lowBattThreshold': {
152
+ if (typeof v === 'string') {
153
+ logQuotes(`${key}.${id}.${k}`)
154
+ }
155
+ const intVal = Number.parseInt(v, 10)
156
+ if (Number.isNaN(intVal)) {
157
+ logDefault(`${key}.${id}.${k}`, platformConsts.defaultValues[k])
158
+ this.deviceConf[id][k] = platformConsts.defaultValues[k]
159
+ } else if (intVal < platformConsts.minValues[k]) {
160
+ logIncrease(`${key}.${id}.${k}`, platformConsts.minValues[k])
161
+ this.deviceConf[id][k] = platformConsts.minValues[k]
162
+ } else {
163
+ this.deviceConf[id][k] = intVal
164
+ }
165
+ break
166
+ }
167
+ case 'connection':
168
+ case 'showAs': {
169
+ const inSet = platformConsts.allowed[k].includes(v)
170
+ if (typeof v !== 'string' || !inSet) {
171
+ logIgnore(`${key}.${id}.${k}`)
172
+ } else {
173
+ this.deviceConf[id][k] = v === 'default' ? platformConsts.defaultValues[k] : v
174
+ }
175
+ break
176
+ }
177
+ case 'deviceUrl':
178
+ case 'firmwareRevision':
179
+ case 'ignoreSubdevices':
180
+ case 'model':
181
+ case 'name':
182
+ case 'serialNumber':
183
+ case 'temperatureSource':
184
+ case 'userkey':
185
+ if (typeof v !== 'string' || v === '') {
186
+ logIgnore(`${key}.${id}.${k}`)
187
+ } else {
188
+ this.deviceConf[id][k] = v.trim()
189
+ if (k === 'deviceUrl') {
190
+ this.localUUIDs.push(id)
191
+ }
192
+ }
193
+ break
194
+ case 'hideChannels': {
195
+ if (typeof v !== 'string' || v === '') {
196
+ logIgnore(`${key}.${id}.${k}`)
197
+ } else {
198
+ const channels = v.split(',')
199
+ channels.forEach((channel) => {
200
+ this.hideChannels.push(id + channel.replace(/\D+/g, ''))
201
+ this.deviceConf[id][k] = v
202
+ })
203
+ }
204
+ break
205
+ }
206
+ case 'ignoreDevice':
207
+ if (typeof v === 'string') {
208
+ logQuotes(`${key}.${id}.${k}`)
209
+ }
210
+ if (!!v && v !== 'false') {
211
+ this.ignoredDevices.push(id)
212
+ }
213
+ break
214
+ case 'reversePolarity':
215
+ if (typeof v === 'string') {
216
+ logQuotes(`${key}.${id}.${k}`)
217
+ }
218
+ this.deviceConf[id][k] = v === 'false' ? false : !!v
219
+ break
220
+ default:
221
+ logRemove(`${key}.${id}.${k}`)
222
+ }
223
+ })
224
+ })
225
+ } else {
226
+ logIgnore(key)
227
+ }
228
+ break
229
+ case 'cloudRefreshRate':
230
+ case 'refreshRate': {
231
+ if (typeof val === 'string') {
232
+ logQuotes(key)
233
+ }
234
+ const intVal = Number.parseInt(val, 10)
235
+ if (Number.isNaN(intVal)) {
236
+ logDefault(key, platformConsts.defaultValues[key])
237
+ } else if (intVal !== 0 && intVal < platformConsts.minValues[key]) {
238
+ logIncrease(key, platformConsts.minValues[key])
239
+ } else if (intVal === 0 || intVal > 600) {
240
+ this.config[key] = 600
241
+ } else {
242
+ this.config[key] = intVal
243
+ }
244
+ break
245
+ }
246
+ case 'connection': {
247
+ const inSet = platformConsts.allowed[key].includes(val)
248
+ if (typeof val !== 'string' || !inSet) {
249
+ logIgnore(key)
250
+ } else {
251
+ this.config[key] = val === 'default' ? platformConsts.defaultValues[key] : val
252
+ }
253
+ break
254
+ }
255
+ case 'disableDeviceLogging':
256
+ case 'ignoreHKNative':
257
+ case 'ignoreMatter':
258
+ case 'showUserKey':
259
+ if (typeof val === 'string') {
260
+ logQuotes(key)
261
+ }
262
+ this.config[key] = val === 'false' ? false : !!val
263
+ break
264
+ case 'domain':
265
+ case 'mfaCode':
266
+ case 'password':
267
+ case 'username':
268
+ if (typeof val !== 'string') {
269
+ logIgnore(key)
270
+ } else {
271
+ this.config[key] = val
272
+ }
273
+ break
274
+ case 'name':
275
+ case 'platform':
276
+ break
277
+ case 'userkey':
278
+ if (typeof val !== 'string') {
279
+ logIgnore(key)
280
+ } else {
281
+ const userkey = val.toLowerCase().replace(/[^a-z\d]+/g, '')
282
+ if (userkey.length === 32) {
283
+ this.config[key] = userkey
284
+ } else {
285
+ logIgnore(key)
286
+ }
287
+ }
288
+ break
289
+ default:
290
+ logRemove(key)
291
+ break
292
+ }
293
+ })
294
+ }
295
+
296
+ async pluginSetup() {
297
+ // Plugin has finished initialising so now onto setup
298
+ try {
299
+ // Log that the plugin initialisation has been successful
300
+ this.log('%s.', platformLang.initialised)
301
+
302
+ // Sort out some logging functions
303
+ if (this.isBeta) {
304
+ this.log.debug = this.log
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
+ } else {
315
+ this.log.debug = () => {}
316
+ this.log.debugWarn = () => {}
317
+ }
318
+
319
+ // Require any libraries that the accessory instances use
320
+ this.cusChar = new platformChars(this.api)
321
+ this.eveChar = new eveChars(this.api)
322
+ this.eveService = eveService(this.api)
323
+
324
+ const cachePath = join(this.api.user.storagePath(), '/bwp91_cache')
325
+
326
+ // Create folders if they don't exist
327
+ if (!existsSync(cachePath)) {
328
+ mkdirSync(cachePath)
329
+ }
330
+
331
+ // Persist files are used to store device info that can be used by my other plugins
332
+ try {
333
+ this.storageData = storage.create({
334
+ dir: cachePath,
335
+ forgiveParseErrors: true,
336
+ })
337
+ await this.storageData.init()
338
+ this.storageClientData = true
339
+ } catch (err) {
340
+ this.log.debugWarn('%s %s.', platformLang.storageSetupErr, parseError(err))
341
+ }
342
+
343
+ // If the user has configured cloud username and password then get a device list
344
+ this.accountDetails = {}
345
+ let cloudDevices = []
346
+ try {
347
+ if (!this.config.username || !this.config.password) {
348
+ throw new Error(platformLang.missingCreds)
349
+ }
350
+
351
+ // Try and get token from the cache to get a device list
352
+ try {
353
+ const storedData = await this.storageData.getItem('Meross_All_Devices_temp')
354
+ const splitData = storedData?.split(':::')
355
+ if (!Array.isArray(splitData) || splitData.length !== 5) {
356
+ throw new Error(platformLang.accTokenNoExist)
357
+ }
358
+ if (splitData[0] !== this.config.username) {
359
+ // Username has changed so throw error to generate new token
360
+ throw new Error(platformLang.accTokenUserChange)
361
+ }
362
+
363
+ this.accountDetails.key = splitData[1]
364
+ this.accountDetails.token = splitData[2]
365
+ this.accountDetails.userId = splitData[3]
366
+ this.accountDetails.domain = splitData[4]
367
+
368
+ this.log.debug('[HTTP] %s.', platformLang.accTokenFromCache)
369
+
370
+ this.cloudClient = new httpClient(this)
371
+ cloudDevices = await this.cloudClient.getDevices()
372
+ } catch (err) {
373
+ this.log.warn('[HTTP] %s %s.', platformLang.accTokenFail, parseError(err, [
374
+ platformLang.accTokenUserChange,
375
+ platformLang.accTokenNoExist,
376
+ platformLang.accTokenInvalid,
377
+ ]))
378
+
379
+ // Remove existing cache info if it exists
380
+ await this.storageData.removeItem('Meross_All_Devices_temp')
381
+
382
+ this.cloudClient = new httpClient(this)
383
+ this.accountDetails = await this.cloudClient.login()
384
+ cloudDevices = await this.cloudClient.getDevices()
385
+ }
386
+
387
+ // Initialise the cloud configured devices into Homebridge
388
+ cloudDevices.forEach(device => this.initialiseDevice(device))
389
+ } catch (err) {
390
+ const eText = parseError(err, [platformLang.mfaFail, platformLang.missingCreds, platformLang.accTokenInvalid])
391
+ this.log.warn('%s %s.', platformLang.disablingCloud, eText)
392
+ this.cloudClient = false
393
+ this.accountDetails = {
394
+ key: this.config.userkey,
395
+ }
396
+ }
397
+
398
+ // Check if a user key has been configured if the credentials aren't present
399
+ if (!this.cloudClient) {
400
+ if (this.config.userkey) {
401
+ // Initialise the local configured devices into Homebridge
402
+ Object.values(this.deviceConf)
403
+ .filter(el => el.deviceUrl)
404
+ .forEach((device) => {
405
+ // Ensure we have a model property if a user key is configured, and credentials are not
406
+ if (!this.config.username && this.config.userkey && !device.model) {
407
+ this.log.warn('[%s] %s.', device.name, platformLang.missingModal)
408
+ return
409
+ }
410
+
411
+ // Rename some properties to fit the format of a cloud device
412
+ // Local devices don't have the uuid already set
413
+ device.uuid = device.serialNumber
414
+ device.deviceType = device.model.toUpperCase().replace(/-+/g, '')
415
+ device.devName = device.name
416
+ device.channels = []
417
+
418
+ // Retrieve how many channels this device has
419
+ const garageCount = device.deviceType === 'MSG200' ? 3 : 1
420
+ const channelCount = platformConsts.models.switchMulti[device.deviceType] || garageCount
421
+
422
+ // Create a list of channels to fit the format of a cloud device
423
+ if (channelCount > 1) {
424
+ for (let index = 0; index <= channelCount; index += 1) {
425
+ device.channels.push({})
426
+ }
427
+ }
428
+ this.initialiseDevice(device)
429
+ })
430
+ } else {
431
+ // Cloud client disabled and no user key - plugin will be useless
432
+ throw new Error(platformLang.noCredentials)
433
+ }
434
+ }
435
+
436
+ // Check for redundant accessories or those that have been ignored but exist
437
+ this.devicesInHB.forEach((accessory) => {
438
+ switch (accessory.context.connection) {
439
+ case 'cloud':
440
+ case 'hybrid':
441
+ if (!cloudDevices.some(el => el.uuid === accessory.context.serialNumber)) {
442
+ this.removeAccessory(accessory)
443
+ }
444
+ break
445
+ case 'local':
446
+ if (!this.localUUIDs.includes(accessory.context.serialNumber)) {
447
+ this.removeAccessory(accessory)
448
+ }
449
+ break
450
+ default:
451
+ // Should never happen
452
+ this.removeAccessory(accessory)
453
+ break
454
+ }
455
+ })
456
+
457
+ // Setup successful
458
+ this.log('%s. %s', platformLang.complete, platformLang.welcome)
459
+ } catch (err) {
460
+ // Catch any errors during setup
461
+ const eText = parseError(err, [platformLang.noCredentials])
462
+ this.log.warn('***** %s. *****', platformLang.disabling)
463
+ this.log.warn('***** %s. *****', eText)
464
+ this.pluginShutdown()
465
+ }
466
+ }
467
+
468
+ pluginShutdown() {
469
+ // A function that is called when the plugin fails to load or Homebridge restarts
470
+ try {
471
+ // Close the mqtt connection for the accessories with an open connection
472
+ if (this.cloudClient) {
473
+ this.devicesInHB.forEach((accessory) => {
474
+ if (accessory.mqtt) {
475
+ accessory.mqtt.disconnect()
476
+ }
477
+ if (accessory.refreshInterval) {
478
+ clearInterval(accessory.refreshInterval)
479
+ }
480
+ if (accessory.powerInterval) {
481
+ clearInterval(accessory.powerInterval)
482
+ }
483
+ })
484
+ }
485
+ } catch (err) {
486
+ // No need to show errors at this point
487
+ }
488
+ }
489
+
490
+ applyAccessoryLogging(accessory) {
491
+ if (this.isBeta) {
492
+ accessory.log = msg => this.log('[%s] %s.', accessory.displayName, msg)
493
+ accessory.logWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
494
+ accessory.logDebug = msg => this.log('[%s] %s.', accessory.displayName, msg)
495
+ accessory.logDebugWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
496
+ } else {
497
+ if (this.config.disableDeviceLogging) {
498
+ accessory.log = () => {}
499
+ accessory.logWarn = () => {}
500
+ } else {
501
+ accessory.log = msg => this.log('[%s] %s.', accessory.displayName, msg)
502
+ accessory.logWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
503
+ }
504
+ accessory.logDebug = () => {}
505
+ accessory.logDebugWarn = () => {}
506
+ }
507
+ }
508
+
509
+ async initialiseDevice(device) {
510
+ try {
511
+ // Get any user configured entry for this device
512
+ const deviceConf = this.deviceConf[device.uuid.toLowerCase()] || {}
513
+
514
+ // Generate a unique id for the accessory
515
+ const hbUUID = this.api.hap.uuid.generate(device.uuid)
516
+ device.firmware = deviceConf.firmwareRevision || device.fmwareVersion
517
+ device.hbDeviceId = device.uuid
518
+ device.model = device.deviceType.toUpperCase().replace(/-+/g, '')
519
+
520
+ // Add context information for the plugin-ui and instance to use
521
+ const context = {
522
+ channel: 0,
523
+ channelCount: device.channels.length,
524
+ connection: deviceConf.deviceUrl
525
+ ? 'local'
526
+ : deviceConf.connection || this.config.connection,
527
+ deviceUrl: deviceConf.deviceUrl,
528
+ domain: device.domain,
529
+ firmware: device.firmware,
530
+ hidden: false,
531
+ isOnline: false,
532
+ model: device.model,
533
+ options: deviceConf,
534
+ serialNumber: device.uuid,
535
+ userkey: deviceConf.userkey || this.accountDetails.key,
536
+ }
537
+
538
+ // Find the correct instance determined by the device model
539
+ let accessory
540
+ if (platformConsts.models.switchSingle.includes(device.model)) {
541
+ /**
542
+ **************
543
+ SWITCHES (SINGLE)
544
+ ***************
545
+ */
546
+ // Set up the accessory and instance
547
+ accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
548
+ accessory.context = { ...accessory.context, ...context }
549
+ this.applyAccessoryLogging(accessory)
550
+ switch (deviceConf.showAs) {
551
+ case 'cooler':
552
+ accessory.control = new deviceTypes.deviceCoolerSingle(this, accessory)
553
+ break
554
+ case 'heater':
555
+ accessory.control = new deviceTypes.deviceHeaterSingle(this, accessory)
556
+ break
557
+ case 'outlet':
558
+ accessory.control = new deviceTypes.deviceOutletSingle(this, accessory)
559
+ break
560
+ case 'purifier':
561
+ accessory.control = new deviceTypes.devicePurifierSingle(this, accessory)
562
+ break
563
+ default:
564
+ accessory.control = new deviceTypes.deviceSwitchSingle(this, accessory)
565
+ }
566
+ /** */
567
+ } else if (hasProperty(platformConsts.models.switchMulti, device.model)) {
568
+ /**
569
+ *************
570
+ SWITCHES (MULTI)
571
+ **************
572
+ */
573
+ // If the user has enabled the option to configure multi-outlet devices as power strips
574
+ if (deviceConf.showAs === 'power-strip') {
575
+ // Set up the main power strip accessory
576
+ accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
577
+ accessory.context = { ...accessory.context, ...context, channels: device.channels }
578
+ this.applyAccessoryLogging(accessory)
579
+ accessory.control = new deviceTypes.devicePowerStrip(this, accessory)
580
+
581
+ // Check to see if there are any leftover accessory instances from before enabling this option
582
+ for (let i = 0; i <= 7; i += 1) {
583
+ const uuidSub = device.uuid + i
584
+ const hbUUIDSub = this.api.hap.uuid.generate(uuidSub)
585
+ if (this.devicesInHB.has(hbUUIDSub)) {
586
+ this.removeAccessory(this.devicesInHB.get(hbUUIDSub))
587
+ }
588
+ }
589
+ } else {
590
+ // Check to see if we need to remove any leftover power strip accessory instances
591
+ if (this.devicesInHB.has(hbUUID)) {
592
+ this.removeAccessory(this.devicesInHB.get(hbUUID))
593
+ }
594
+
595
+ // Loop through the channels
596
+ device.channels.forEach((channel, index) => {
597
+ const subdeviceObj = { ...device }
598
+ const extraContext = {}
599
+
600
+ // Generate the Homebridge UUID from the device uuid and channel index
601
+ const uuidSub = device.uuid + index
602
+ subdeviceObj.hbDeviceId = uuidSub
603
+ const hbUUIDSub = this.api.hap.uuid.generate(uuidSub)
604
+
605
+ // Supply a device name for the channel accessories
606
+ if (index > 0) {
607
+ subdeviceObj.devName = channel.devName || `${device.devName} SW${index}`
608
+ }
609
+
610
+ // Check if the user has chosen to hide any channels for this device
611
+ let subAcc
612
+ if (this.hideChannels.includes(device.uuid + index)) {
613
+ // The user has hidden this channel so if it exists then remove it
614
+ if (this.devicesInHB.has(hbUUIDSub)) {
615
+ this.removeAccessory(this.devicesInHB.get(hbUUIDSub))
616
+ }
617
+
618
+ // If this is the main channel then add it to the array of hidden masters
619
+ if (index === 0) {
620
+ this.hideMasters.push(device.uuid)
621
+
622
+ // Add the sub accessory, but hidden, to Homebridge
623
+ extraContext.hidden = true
624
+ subAcc = this.addAccessory(subdeviceObj, true)
625
+ } else {
626
+ return
627
+ }
628
+ } else {
629
+ // The user has not hidden this channel
630
+ subAcc = this.devicesInHB.get(hbUUIDSub) || this.addAccessory(subdeviceObj)
631
+ }
632
+
633
+ // Add the context information to the accessory
634
+ extraContext.channel = index
635
+ subAcc.context = { ...subAcc.context, ...context, ...extraContext }
636
+ this.applyAccessoryLogging(subAcc)
637
+
638
+ // Create the device type instance for this accessory
639
+ switch (deviceConf.showAs) {
640
+ case 'outlet':
641
+ subAcc.control = new deviceTypes.deviceOutletMulti(this, subAcc)
642
+ break
643
+ default:
644
+ subAcc.control = new deviceTypes.deviceSwitchMulti(this, subAcc)
645
+ break
646
+ }
647
+
648
+ // This is used for later in this function for logging
649
+ if (index === 0) {
650
+ accessory = subAcc
651
+ } else {
652
+ // Update any changes to the accessory to the platform
653
+ this.api.updatePlatformAccessories([subAcc])
654
+ this.devicesInHB.set(subAcc.UUID, subAcc)
655
+ }
656
+ })
657
+ }
658
+ /** */
659
+ } else if (platformConsts.models.lightDimmer.includes(device.model)) {
660
+ /**
661
+ ************
662
+ LIGHTS (DIMMER)
663
+ *************
664
+ */
665
+ accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
666
+ accessory.context = { ...accessory.context, ...context }
667
+ this.applyAccessoryLogging(accessory)
668
+ accessory.control = new deviceTypes.deviceLightDimmer(this, accessory)
669
+ /** */
670
+ } else if (platformConsts.models.lightRGB.includes(device.model)) {
671
+ /**
672
+ *********
673
+ LIGHTS (RGB)
674
+ **********
675
+ */
676
+ accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
677
+ accessory.context = { ...accessory.context, ...context }
678
+ this.applyAccessoryLogging(accessory)
679
+ accessory.control = new deviceTypes.deviceLightRGB(this, accessory)
680
+ /** */
681
+ } else if (platformConsts.models.lightCCT.includes(device.model)) {
682
+ /**
683
+ *********
684
+ LIGHTS (CCT)
685
+ **********
686
+ */
687
+ accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
688
+ accessory.context = { ...accessory.context, ...context }
689
+ this.applyAccessoryLogging(accessory)
690
+ accessory.control = new deviceTypes.deviceLightCCT(this, accessory)
691
+ /** */
692
+ } else if (platformConsts.models.garage.includes(device.model)) {
693
+ /**
694
+ *********
695
+ GARAGE DOORS
696
+ **********
697
+ */
698
+ if (device.model === 'MSG200') {
699
+ // If a main accessory exists from before then remove it so re-added as hidden
700
+ if (this.devicesInHB.has(hbUUID)) {
701
+ this.removeAccessory(this.devicesInHB.get(hbUUID))
702
+ }
703
+
704
+ // First, set up the main, hidden, accessory that will process the control and updates
705
+ accessory = this.addAccessory(device, true)
706
+ accessory.context = { ...accessory.context, ...context, hidden: true }
707
+ this.applyAccessoryLogging(accessory)
708
+ accessory.control = new deviceTypes.deviceGarageMain(this, accessory)
709
+
710
+ // Loop through the channels
711
+ device.channels.forEach((channel, index) => {
712
+ // Skip the channel 0 entry
713
+ if (index === 0) {
714
+ return
715
+ }
716
+ const subdeviceObj = { ...device }
717
+ const extraContext = {}
718
+
719
+ // Generate the Homebridge UUID from the device uuid and channel index
720
+ const uuidSub = device.uuid + index
721
+ subdeviceObj.hbDeviceId = uuidSub
722
+ const hbUUIDSub = this.api.hap.uuid.generate(uuidSub)
723
+
724
+ // Supply a device name for the channel accessories
725
+ if (index > 0) {
726
+ device.devName = channel.devName || `${device.devName} SW${index}`
727
+ }
728
+
729
+ // Check if the user has chosen to hide any channels for this device
730
+ if (this.hideChannels.includes(device.uuid + index)) {
731
+ // The user has hidden this channel so if it exists then remove it
732
+ if (this.devicesInHB.has(hbUUIDSub)) {
733
+ this.removeAccessory(this.devicesInHB.get(hbUUIDSub))
734
+ }
735
+ return
736
+ }
737
+
738
+ // The user has not hidden this channel
739
+ const subAcc = this.devicesInHB.get(hbUUIDSub) || this.addAccessory(subdeviceObj)
740
+
741
+ // Add the context information to the accessory
742
+ extraContext.channel = index
743
+ subAcc.context = { ...subAcc.context, ...context, ...extraContext }
744
+ this.applyAccessoryLogging(subAcc)
745
+
746
+ // Create the device type instance for this accessory
747
+ subAcc.control = new deviceTypes.deviceGarageSub(this, subAcc, accessory)
748
+
749
+ // Update any changes to the accessory to the platform
750
+ this.api.updatePlatformAccessories([subAcc])
751
+ this.devicesInHB.set(subAcc.UUID, subAcc)
752
+ })
753
+ } else {
754
+ accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
755
+ accessory.context = { ...accessory.context, ...context }
756
+ this.applyAccessoryLogging(accessory)
757
+ accessory.control = new deviceTypes.deviceGarageSingle(this, accessory)
758
+ }
759
+ /** */
760
+ } else if (platformConsts.models.roller.includes(device.model)) {
761
+ /**
762
+ ***********
763
+ ROLLING MOTORS
764
+ ************
765
+ */
766
+ accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
767
+ accessory.context = { ...accessory.context, ...context }
768
+ this.applyAccessoryLogging(accessory)
769
+ accessory.control = ['6.0.0', '7.0.0', '8.0.0'].includes(device.hdwareVersion)
770
+ ? new deviceTypes.deviceRollerLocation(this, accessory)
771
+ : new deviceTypes.deviceRoller(this, accessory)
772
+ /** */
773
+ } else if (platformConsts.models.purifier.includes(device.model)) {
774
+ /**
775
+ ******
776
+ PURIFIERS
777
+ *******
778
+ */
779
+ accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
780
+ accessory.context = { ...accessory.context, ...context }
781
+ this.applyAccessoryLogging(accessory)
782
+ accessory.control = new deviceTypes.devicePurifier(this, accessory)
783
+ /** */
784
+ } else if (platformConsts.models.fan.includes(device.model)) {
785
+ /**
786
+ FANS
787
+ *
788
+ */
789
+ accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
790
+ accessory.context = { ...accessory.context, ...context }
791
+ this.applyAccessoryLogging(accessory)
792
+ accessory.control = new deviceTypes.deviceFan(this, accessory)
793
+ /** */
794
+ } else if (platformConsts.models.diffuser.includes(device.model)) {
795
+ /**
796
+ ******
797
+ DIFFUSERS
798
+ *******
799
+ */
800
+ accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
801
+ accessory.context = { ...accessory.context, ...context }
802
+ this.applyAccessoryLogging(accessory)
803
+ accessory.control = new deviceTypes.deviceDiffuser(this, accessory)
804
+ /** */
805
+ } else if (platformConsts.models.humidifier.includes(device.model)) {
806
+ /**
807
+ ********
808
+ HUMIDIFIERS
809
+ *********
810
+ */
811
+ accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
812
+ accessory.context = { ...accessory.context, ...context }
813
+ this.applyAccessoryLogging(accessory)
814
+ accessory.control = new deviceTypes.deviceHumidifier(this, accessory)
815
+ /** */
816
+ } else if (platformConsts.models.baby.includes(device.model)) {
817
+ /**
818
+ **********
819
+ BABY MONITORS
820
+ ***********
821
+ */
822
+ accessory = this.addExternalAccessory(device, 26)
823
+ accessory.context = { ...accessory.context, ...context }
824
+ this.applyAccessoryLogging(accessory)
825
+
826
+ // Create a second accessory for the baby light
827
+ const deviceLightHBID = `${device.uuid}_light`
828
+ const deviceLightHBUUID = this.api.hap.uuid.generate(deviceLightHBID)
829
+ const deviceLight = {
830
+ ...device,
831
+ hbDeviceId: deviceLightHBID,
832
+ }
833
+ const accessoryLight = this.devicesInHB.get(deviceLightHBUUID) || this.addAccessory(deviceLight)
834
+ accessoryLight.context = { ...accessory.context, ...context }
835
+ this.applyAccessoryLogging(accessoryLight)
836
+
837
+ // Update any changes to the accessory to the platform
838
+ this.api.updatePlatformAccessories([accessoryLight])
839
+ this.devicesInHB.set(accessoryLight.UUID, accessoryLight)
840
+
841
+ // Set up the main accessory for the baby monitor
842
+ accessory.control = new deviceTypes.deviceBaby(this, accessory, accessoryLight)
843
+ /** */
844
+ } else if (platformConsts.models.thermostat.includes(device.model)) {
845
+ /**
846
+ ********
847
+ THERMOSTATS
848
+ *********
849
+ */
850
+ accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
851
+ accessory.context = { ...accessory.context, ...context }
852
+ this.applyAccessoryLogging(accessory)
853
+ accessory.control = new deviceTypes.deviceThermostat(this, accessory)
854
+ /** */
855
+ } else if (platformConsts.models.hubMain.includes(device.model)) {
856
+ /**
857
+ ********
858
+ SENSOR HUBS
859
+ *********
860
+ */
861
+ // At the moment, cloud connection is necessary to get a subdevice list
862
+ if (!this.cloudClient) {
863
+ throw new Error(platformLang.sensorNoCloud)
864
+ }
865
+
866
+ // Obtain array of any subdevices to ignore
867
+ const subdevicesToIgnore = []
868
+ if (context.options.ignoreSubdevices) {
869
+ context.options.ignoreSubdevices
870
+ .split(',')
871
+ .forEach(subdeviceId => subdevicesToIgnore.push(subdeviceId.trim()))
872
+ }
873
+ context.ignoreSubdevices = subdevicesToIgnore
874
+
875
+ // First, set up the main, hidden, accessory that will process the incoming updates
876
+ accessory = this.addAccessory(device, true)
877
+ accessory.context = { ...accessory.context, ...context, hidden: true }
878
+ this.applyAccessoryLogging(accessory)
879
+ accessory.control = new deviceTypes.deviceHubMain(this, accessory)
880
+
881
+ // Then request and initialise a list of subdevices
882
+ const subdevices = await this.cloudClient.getSubDevices(device)
883
+ if (!Array.isArray(subdevices)) {
884
+ throw new TypeError(platformLang.sensorNoSubs)
885
+ }
886
+
887
+ // Initialise subdevices into HB
888
+ subdevices.forEach((subdevice) => {
889
+ try {
890
+ // Create an object to mimic the addAccessory data
891
+ const subdeviceObj = { ...device }
892
+ const uuidSub = device.uuid + subdevice.subDeviceId
893
+ const hbUUIDSub = this.api.hap.uuid.generate(uuidSub)
894
+
895
+ // Check if it's ignored device
896
+ if (subdevicesToIgnore.includes(subdevice.subDeviceId)) {
897
+ // Is ignored, remove if exists
898
+ if (this.devicesInHB.has(hbUUIDSub)) {
899
+ this.removeAccessory(this.devicesInHB.get(hbUUIDSub))
900
+ }
901
+ return
902
+ }
903
+
904
+ // Not ignored, so continue initialising
905
+ subdeviceObj.devName = subdevice.subDeviceName || subdevice.subDeviceId
906
+ subdeviceObj.hbDeviceId = uuidSub
907
+ subdeviceObj.model = subdevice.subDeviceType.toUpperCase().replace(/-+/g, '')
908
+
909
+ // Check the subdevice model is supported
910
+ if (!platformConsts.models.hubSub.includes(subdeviceObj.model)) {
911
+ // Not supported, so show a log message with helpful info for a GitHub issue
912
+ this.log.warn(
913
+ '[%s] %s:\n%s',
914
+ subdeviceObj.devName,
915
+ platformLang.notSupp,
916
+ JSON.stringify(subdeviceObj),
917
+ )
918
+ return
919
+ }
920
+
921
+ // Obtain or add this subdevice to Homebridge
922
+ const subAcc = this.devicesInHB.get(hbUUIDSub) || this.addAccessory(subdeviceObj)
923
+
924
+ // Add helpful context info to the accessory object
925
+ subAcc.context = {
926
+ ...subAcc.context,
927
+ ...context,
928
+ subSerialNumber: subdevice.subDeviceId,
929
+ }
930
+ this.applyAccessoryLogging(subAcc)
931
+
932
+ // Create the device type instance for this accessory
933
+ switch (subdeviceObj.model) {
934
+ case 'GS559A':
935
+ subAcc.control = new deviceTypes.deviceHubSmoke(this, subAcc)
936
+ break
937
+ case 'MS100':
938
+ case 'MS100F':
939
+ subAcc.control = new deviceTypes.deviceHubSensor(this, subAcc)
940
+ break
941
+ case 'MS200':
942
+ subAcc.control = new deviceTypes.deviceHubContact(this, subAcc)
943
+ break
944
+ case 'MS400':
945
+ subAcc.control = new deviceTypes.deviceHubLeak(this, subAcc)
946
+ break
947
+ case 'MTS100V3':
948
+ case 'MTS150':
949
+ subAcc.control = new deviceTypes.deviceHubValve(this, subAcc, accessory)
950
+ break
951
+ default:
952
+ return
953
+ }
954
+
955
+ // Update any changes to the accessory to the platform
956
+ this.api.updatePlatformAccessories([subAcc])
957
+ this.devicesInHB.set(subAcc.UUID, subAcc)
958
+
959
+ // Log the subdevice id so a user can use it to ignore device if wanted
960
+ this.log(
961
+ '[%s] [%s] %s [%s].',
962
+ device.devName,
963
+ subdeviceObj.devName,
964
+ platformLang.devSubInit,
965
+ subdevice.subDeviceId,
966
+ )
967
+ } catch (err) {
968
+ this.log.warn('[%s] %s %s.', subdevice.subDeviceName, platformLang.devNotAdd, parseError(err))
969
+ }
970
+ })
971
+ /** */
972
+ } else if (platformConsts.models.sensorPresence.includes(device.model)) {
973
+ /**
974
+ *****************
975
+ SENSOR (PRESENCE)
976
+ *****************
977
+ */
978
+ accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
979
+ accessory.context = { ...accessory.context, ...context }
980
+ this.applyAccessoryLogging(accessory)
981
+ accessory.control = new deviceTypes.deviceSensorPresence(this, accessory)
982
+ /** */
983
+ } else if (platformConsts.models.template.includes(device.model)) {
984
+ /**
985
+ ****************
986
+ WORK IN PROGRESS
987
+ ****************
988
+ */
989
+ accessory = this.devicesInHB.get(hbUUID) || this.addAccessory(device)
990
+ accessory.context = { ...accessory.context, ...context }
991
+ this.applyAccessoryLogging(accessory)
992
+ accessory.control = new deviceTypes.deviceTemplate(this, accessory)
993
+ /** */
994
+ } else {
995
+ /**
996
+ *************
997
+ UNSUPPORTED YET
998
+ *************
999
+ */
1000
+ this.log.warn('[%s] %s:\n%s', device.devName, platformLang.notSupp, JSON.stringify(device))
1001
+ return
1002
+ /** */
1003
+ }
1004
+
1005
+ // Log the device initialisation
1006
+ accessory.log(`${platformLang.devInit} [${device.uuid}]`)
1007
+
1008
+ // Extra debug logging when set, show the device JSON info
1009
+ accessory.logDebug(`${platformLang.jsonInfo}: ${JSON.stringify(device)}`)
1010
+
1011
+ // Update any changes to the accessory to the platform
1012
+ this.api.updatePlatformAccessories([accessory])
1013
+ this.devicesInHB.set(accessory.UUID, accessory)
1014
+ } catch (err) {
1015
+ // Catch any errors during device initialisation
1016
+ const eText = parseError(err, [
1017
+ platformLang.accNotFound,
1018
+ platformLang.sensorNoCloud,
1019
+ platformLang.sensorNoSubs,
1020
+ ])
1021
+ this.log.warn('[%s] %s %s.', device.devName, platformLang.devNotInit, eText)
1022
+ }
1023
+ }
1024
+
1025
+ addAccessory(device, hidden = false) {
1026
+ // Add an accessory to Homebridge
1027
+ try {
1028
+ const accessory = new this.api.platformAccessory(
1029
+ device.devName,
1030
+ this.api.hap.uuid.generate(device.hbDeviceId),
1031
+ )
1032
+
1033
+ // If it isn't a hidden device then set the accessory characteristics
1034
+ if (!hidden) {
1035
+ accessory
1036
+ .getService(this.api.hap.Service.AccessoryInformation)
1037
+ .setCharacteristic(this.api.hap.Characteristic.Name, device.devName)
1038
+ .setCharacteristic(this.api.hap.Characteristic.ConfiguredName, device.devName)
1039
+ .setCharacteristic(this.api.hap.Characteristic.SerialNumber, device.uuid)
1040
+ .setCharacteristic(this.api.hap.Characteristic.Manufacturer, platformLang.brand)
1041
+ .setCharacteristic(this.api.hap.Characteristic.Model, device.model)
1042
+ .setCharacteristic(
1043
+ this.api.hap.Characteristic.FirmwareRevision,
1044
+ device.firmware || plugin.version,
1045
+ )
1046
+ .setCharacteristic(this.api.hap.Characteristic.Identify, true)
1047
+
1048
+ // Register the accessory if it hasn't been hidden by the user
1049
+ this.api.registerPlatformAccessories(plugin.name, plugin.alias, [accessory])
1050
+ this.log('[%s] %s.', device.devName, platformLang.devAdd)
1051
+ }
1052
+
1053
+ // Configure for good practice
1054
+ this.configureAccessory(accessory)
1055
+
1056
+ // Return the new accessory
1057
+ return accessory
1058
+ } catch (err) {
1059
+ // Catch any errors during add
1060
+ this.log.warn('[%s] %s %s.', device.devName, platformLang.devNotAdd, parseError(err))
1061
+ return false
1062
+ }
1063
+ }
1064
+
1065
+ addExternalAccessory(device, category) {
1066
+ try {
1067
+ // Add the new accessory to Homebridge
1068
+ const accessory = new this.api.platformAccessory(
1069
+ device.devName,
1070
+ this.api.hap.uuid.generate(device.hbDeviceId),
1071
+ category,
1072
+ )
1073
+
1074
+ // Set the accessory characteristics
1075
+ accessory
1076
+ .getService(this.api.hap.Service.AccessoryInformation)
1077
+ .setCharacteristic(this.api.hap.Characteristic.Name, device.devName)
1078
+ .setCharacteristic(this.api.hap.Characteristic.ConfiguredName, device.devName)
1079
+ .setCharacteristic(this.api.hap.Characteristic.SerialNumber, device.uuid)
1080
+ .setCharacteristic(this.api.hap.Characteristic.Manufacturer, platformLang.brand)
1081
+ .setCharacteristic(this.api.hap.Characteristic.Model, device.model)
1082
+ .setCharacteristic(
1083
+ this.api.hap.Characteristic.FirmwareRevision,
1084
+ device.firmware || plugin.version,
1085
+ )
1086
+ .setCharacteristic(this.api.hap.Characteristic.Identify, true)
1087
+
1088
+ // Register the accessory
1089
+ this.api.publishExternalAccessories(plugin.name, [accessory])
1090
+ this.log('[%s] %s.', device.devName, platformLang.devAdd)
1091
+
1092
+ // Return the new accessory
1093
+ this.configureAccessory(accessory)
1094
+ return accessory
1095
+ } catch (err) {
1096
+ // Catch any errors during add
1097
+ this.log.warn('[%s] %s %s.', device.name, platformLang.devNotAdd, parseError(err))
1098
+ return false
1099
+ }
1100
+ }
1101
+
1102
+ configureAccessory(accessory) {
1103
+ // Set the correct firmware version if we can
1104
+ if (this.api && accessory.context.firmware) {
1105
+ accessory
1106
+ .getService(this.api.hap.Service.AccessoryInformation)
1107
+ .updateCharacteristic(
1108
+ this.api.hap.Characteristic.FirmwareRevision,
1109
+ accessory.context.firmware,
1110
+ )
1111
+ }
1112
+
1113
+ // Add the configured accessory to our global map
1114
+ this.devicesInHB.set(accessory.UUID, accessory)
1115
+ }
1116
+
1117
+ updateAccessory(accessory) {
1118
+ this.api.updatePlatformAccessories([accessory])
1119
+ if (accessory.context.isOnline) {
1120
+ this.log('[%s] %s.', accessory.displayName, platformLang.repOnline)
1121
+ } else {
1122
+ this.log.warn('[%s] %s.', accessory.displayName, platformLang.repOffline)
1123
+ }
1124
+ }
1125
+
1126
+ removeAccessory(accessory) {
1127
+ try {
1128
+ // Remove an accessory from Homebridge
1129
+ if (!accessory.context.hidden) {
1130
+ this.api.unregisterPlatformAccessories(plugin.name, plugin.alias, [accessory])
1131
+ }
1132
+ this.devicesInHB.delete(accessory.UUID)
1133
+ this.log('[%s] %s.', accessory.displayName, platformLang.devRemove)
1134
+ } catch (err) {
1135
+ // Catch any errors during remove
1136
+ this.log.warn('[%s] %s %s.', accessory.displayName, platformLang.devNotRemove, parseError(err))
1137
+ }
1138
+ }
1139
+
1140
+ async sendUpdate(accessory, toSend) {
1141
+ // Variable res is the response from either the cloud mqtt update or local http request
1142
+ let res
1143
+
1144
+ // Generate the method variable determined from an empty payload or not
1145
+ toSend.method = toSend.method || (Object.keys(toSend.payload).length === 0 ? 'GET' : 'SET')
1146
+
1147
+ // Always try local control first, even for cloud devices
1148
+ try {
1149
+ // Check the user has this mode turned on
1150
+ if (accessory.context.connection === 'cloud') {
1151
+ throw new Error(platformLang.noHybridMode)
1152
+ }
1153
+
1154
+ // Check we have the user key
1155
+ if (!accessory.context.userkey) {
1156
+ throw new Error(platformLang.noUserKey)
1157
+ }
1158
+
1159
+ // Certain models aren't supported for local control
1160
+ if (platformConsts.noLocalControl.includes(accessory.context.model)) {
1161
+ throw new Error(platformLang.notSuppLocal)
1162
+ }
1163
+
1164
+ // Obtain the IP address, either manually configured or from Meross polling data
1165
+ const ipAddress = accessory.context.deviceUrl || accessory.context.ipAddress
1166
+
1167
+ // Check the IP address exists
1168
+ if (!ipAddress) {
1169
+ throw new Error(platformLang.noIP)
1170
+ }
1171
+
1172
+ // Generate the timestamp, messageId and sign from the userkey
1173
+ const timestamp = Math.floor(Date.now() / 1000)
1174
+ const messageId = generateRandomString(32)
1175
+ const sign = createHash('md5')
1176
+ .update(messageId + accessory.context.userkey + timestamp)
1177
+ .digest('hex')
1178
+
1179
+ // Generate the payload to send
1180
+ const data = {
1181
+ header: {
1182
+ from: `http://${ipAddress}/config`,
1183
+ messageId,
1184
+ method: toSend.method,
1185
+ namespace: toSend.namespace,
1186
+ payloadVersion: 1,
1187
+ sign,
1188
+ timestamp,
1189
+ triggerSrc: 'iOSLocal',
1190
+ uuid: accessory.context.serialNumber,
1191
+ },
1192
+ payload: toSend.payload || {},
1193
+ }
1194
+
1195
+ // Log the update if user enabled
1196
+ accessory.logDebug(`${platformLang.sendUpdate}: ${JSON.stringify(data)}`)
1197
+
1198
+ // Send the request to the device
1199
+ res = await axios({
1200
+ url: `http://${ipAddress}/config`,
1201
+ method: 'post',
1202
+ headers: { 'content-type': 'application/json' },
1203
+ data,
1204
+ responseType: 'json',
1205
+ timeout: toSend.method === 'GET' || accessory.context.connection === 'local' ? 9000 : 4000,
1206
+ })
1207
+
1208
+ // Check the response properties based on whether it is a control or request update
1209
+ switch (toSend.method) {
1210
+ case 'SET': {
1211
+ // Check the response
1212
+ if (!res.data || !res.data.header || res.data.header.method === 'ERROR') {
1213
+ throw new Error(`${platformLang.reqFail} - ${JSON.stringify(res.data.payload.error)}`)
1214
+ }
1215
+ break
1216
+ }
1217
+ default: { // GET
1218
+ // Validate the response, checking for payload property
1219
+ if (!res.data || !res.data.payload) {
1220
+ throw new Error(platformLang.invalidResponse)
1221
+ }
1222
+
1223
+ // Check we are sending the command to the correct device
1224
+ if (
1225
+ res.data.header.from
1226
+ !== `/appliance/${accessory.context.serialNumber}/publish`
1227
+ ) {
1228
+ throw new Error(platformLang.wrongDevice)
1229
+ }
1230
+ break
1231
+ }
1232
+ }
1233
+ } catch (err) {
1234
+ if (accessory.context.connection === 'local') {
1235
+ // An error occurred and cloud mode is disabled so report the error back
1236
+ throw err
1237
+ } else {
1238
+ // An error occurred, so we can try sending the request via the cloud
1239
+ const eText = parseError(err, [
1240
+ platformLang.noHybridMode,
1241
+ platformLang.notSuppLocal,
1242
+ platformLang.noUserKey,
1243
+ platformLang.noIP,
1244
+ platformLang.wrongDevice,
1245
+ ])
1246
+ accessory.logDebug(`${platformLang.revertToCloud} ${eText}`)
1247
+
1248
+ // Send the update via cloud mqtt
1249
+ res = await accessory.mqtt.sendUpdate(accessory, toSend)
1250
+ }
1251
+ }
1252
+
1253
+ // Return the response
1254
+ return res
1255
+ }
1256
+ }