@homebridge-plugins/homebridge-wemo 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.
Files changed (41) hide show
  1. package/CHANGELOG.md +809 -0
  2. package/LICENSE +21 -0
  3. package/README.md +66 -0
  4. package/config.schema.json +814 -0
  5. package/eslint.config.js +50 -0
  6. package/lib/connection/http.js +174 -0
  7. package/lib/connection/upnp.js +155 -0
  8. package/lib/device/coffee.js +167 -0
  9. package/lib/device/crockpot.js +380 -0
  10. package/lib/device/dimmer.js +280 -0
  11. package/lib/device/heater.js +416 -0
  12. package/lib/device/humidifier.js +379 -0
  13. package/lib/device/index.js +39 -0
  14. package/lib/device/insight.js +353 -0
  15. package/lib/device/lightswitch.js +154 -0
  16. package/lib/device/link-bulb.js +467 -0
  17. package/lib/device/link-hub.js +63 -0
  18. package/lib/device/maker-garage.js +426 -0
  19. package/lib/device/maker-switch.js +202 -0
  20. package/lib/device/motion.js +148 -0
  21. package/lib/device/outlet.js +148 -0
  22. package/lib/device/purifier.js +468 -0
  23. package/lib/device/simulation/purifier-insight.js +357 -0
  24. package/lib/device/simulation/purifier.js +159 -0
  25. package/lib/device/simulation/switch-insight.js +315 -0
  26. package/lib/device/simulation/switch.js +154 -0
  27. package/lib/fakegato/LICENSE +21 -0
  28. package/lib/fakegato/fakegato-history.js +814 -0
  29. package/lib/fakegato/fakegato-storage.js +108 -0
  30. package/lib/fakegato/fakegato-timer.js +125 -0
  31. package/lib/fakegato/uuid.js +27 -0
  32. package/lib/homebridge-ui/public/index.html +315 -0
  33. package/lib/homebridge-ui/server.js +10 -0
  34. package/lib/index.js +8 -0
  35. package/lib/platform.js +1423 -0
  36. package/lib/utils/colour.js +135 -0
  37. package/lib/utils/constants.js +129 -0
  38. package/lib/utils/eve-chars.js +130 -0
  39. package/lib/utils/functions.js +52 -0
  40. package/lib/utils/lang-en.js +125 -0
  41. package/package.json +70 -0
@@ -0,0 +1,1423 @@
1
+ import { createServer } from 'node:http'
2
+ import { createRequire } from 'node:module'
3
+ import { networkInterfaces } from 'node:os'
4
+ import process from 'node:process'
5
+ import { URL as url } from 'node:url'
6
+
7
+ import axios from 'axios'
8
+ import ip from 'ip'
9
+ import nodeSSDP from 'node-ssdp'
10
+ import { parseStringPromise } from 'xml2js'
11
+ import { create as xmlCreate } from 'xmlbuilder'
12
+
13
+ import httpClient from './connection/http.js'
14
+ import upnpClient from './connection/upnp.js'
15
+ import deviceTypes from './device/index.js'
16
+ import eveService from './fakegato/fakegato-history.js'
17
+ import platformConsts from './utils/constants.js'
18
+ import eveChars from './utils/eve-chars.js'
19
+ import { parseError, parseSerialNumber } from './utils/functions.js'
20
+ import platformLang from './utils/lang-en.js'
21
+
22
+ const { Client: ssdp } = nodeSSDP
23
+ const require = createRequire(import.meta.url)
24
+ const plugin = require('../package.json')
25
+
26
+ const devicesInHB = new Map()
27
+
28
+ let listenerServer
29
+ let cacheSerialsToConnect = []
30
+ let existSerialsToConnect = []
31
+ let ssdpClient
32
+
33
+ export default class {
34
+ constructor(log, config, api) {
35
+ if (!log || !api) {
36
+ return
37
+ }
38
+
39
+ // Begin plugin initialisation
40
+ try {
41
+ this.api = api
42
+ this.log = log
43
+ this.isBeta = plugin.version.includes('beta')
44
+
45
+ // Configuration objects for accessories
46
+ this.ignoredDevices = []
47
+ this.manualDevices = []
48
+ this.deviceConf = {}
49
+
50
+ // Make sure user is running Homebridge v1.5 or above
51
+ if (!api?.versionGreaterOrEqual('1.5.0')) {
52
+ throw new Error(platformLang.hbVersionFail)
53
+ }
54
+
55
+ // Check the user has configured the plugin
56
+ if (!config) {
57
+ throw new Error(platformLang.notConfigured)
58
+ }
59
+
60
+ // Log some environment info for debugging
61
+ this.log(
62
+ '%s v%s | System %s | Node %s | HB v%s | HAPNodeJS v%s...',
63
+ platformLang.initialising,
64
+ plugin.version,
65
+ process.platform,
66
+ process.version,
67
+ api.serverVersion,
68
+ api.hap.HAPLibraryVersion(),
69
+ )
70
+
71
+ // Apply the user's configuration
72
+ this.config = platformConsts.defaultConfig
73
+ this.applyUserConfig(config)
74
+
75
+ // Set up the Homebridge events
76
+ this.api.on('didFinishLaunching', () => this.pluginSetup())
77
+ this.api.on('shutdown', () => this.pluginShutdown())
78
+ } catch (err) {
79
+ // Catch any errors during initialisation
80
+ const eText = parseError(err, [platformLang.hbVersionFail, platformLang.notConfigured])
81
+ log.warn('***** %s. *****', platformLang.disabling)
82
+ log.warn('***** %s. *****', eText)
83
+ }
84
+ }
85
+
86
+ applyUserConfig(config) {
87
+ // These shorthand functions save line space during config parsing
88
+ const logDefault = (k, def) => {
89
+ this.log.warn('%s [%s] %s %s.', platformLang.cfgItem, k, platformLang.cfgDef, def)
90
+ }
91
+ const logDuplicate = (k) => {
92
+ this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgDup)
93
+ }
94
+ const logIgnore = (k) => {
95
+ this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgIgn)
96
+ }
97
+ const logIgnoreItem = (k) => {
98
+ this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgIgnItem)
99
+ }
100
+ const logIncrease = (k, min) => {
101
+ this.log.warn('%s [%s] %s %s.', platformLang.cfgItem, k, platformLang.cfgLow, min)
102
+ }
103
+ const logQuotes = (k) => {
104
+ this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgQts)
105
+ }
106
+ const logRemove = (k) => {
107
+ this.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgRmv)
108
+ }
109
+
110
+ // Begin applying the user's config
111
+ Object.entries(config).forEach((entry) => {
112
+ const [key, val] = entry
113
+ switch (key) {
114
+ case 'disableDeviceLogging':
115
+ case 'disableUPNP':
116
+ case 'hideConnectionErrors':
117
+ if (typeof val === 'string') {
118
+ logQuotes(key)
119
+ }
120
+ this.config[key] = val === 'false' ? false : !!val
121
+ break
122
+ case 'discoveryInterval':
123
+ case 'pollingInterval':
124
+ case 'upnpInterval': {
125
+ if (typeof val === 'string') {
126
+ logQuotes(key)
127
+ }
128
+ const intVal = Number.parseInt(val, 10)
129
+ if (Number.isNaN(intVal)) {
130
+ logDefault(key, platformConsts.defaultValues[key])
131
+ this.config[key] = platformConsts.defaultValues[key]
132
+ } else if (intVal < platformConsts.minValues[key]) {
133
+ logIncrease(key, platformConsts.minValues[key])
134
+ this.config[key] = platformConsts.minValues[key]
135
+ } else {
136
+ this.config[key] = intVal
137
+ }
138
+ break
139
+ }
140
+ case 'makerTypes':
141
+ case 'wemoInsights':
142
+ case 'wemoLights':
143
+ case 'wemoLinks':
144
+ case 'wemoMotions':
145
+ case 'wemoOthers':
146
+ case 'wemoOutlets':
147
+ if (Array.isArray(val) && val.length > 0) {
148
+ val.forEach((x) => {
149
+ if (!x.serialNumber) {
150
+ logIgnoreItem(key)
151
+ return
152
+ }
153
+ const id = parseSerialNumber(x.serialNumber)
154
+ if (Object.keys(this.deviceConf).includes(id)) {
155
+ logDuplicate(`${key}.${id}`)
156
+ return
157
+ }
158
+ const entries = Object.entries(x)
159
+ if (entries.length === 1) {
160
+ logRemove(`${key}.${id}`)
161
+ return
162
+ }
163
+ this.deviceConf[id] = {}
164
+ cacheSerialsToConnect.push(id)
165
+ entries.forEach((subEntry) => {
166
+ const [k, v] = subEntry
167
+ if (!platformConsts.allowed[key].includes(k)) {
168
+ logRemove(`${key}.${id}.${k}`)
169
+ return
170
+ }
171
+ switch (k) {
172
+ case 'adaptiveLightingShift':
173
+ case 'brightnessStep':
174
+ case 'makerTimer':
175
+ case 'noMotionTimer':
176
+ case 'timeDiff':
177
+ case 'transitionTime':
178
+ case 'wattDiff': {
179
+ if (typeof v === 'string') {
180
+ logQuotes(`${key}.${id}.${k}`)
181
+ }
182
+ const intVal = Number.parseInt(v, 10)
183
+ if (Number.isNaN(intVal)) {
184
+ logDefault(`${key}.${id}.${k}`, platformConsts.defaultValues[k])
185
+ this.deviceConf[id][k] = platformConsts.defaultValues[k]
186
+ } else if (intVal < platformConsts.minValues[k]) {
187
+ logIncrease(`${key}.${id}.${k}`, platformConsts.minValues[k])
188
+ this.deviceConf[id][k] = platformConsts.minValues[k]
189
+ } else {
190
+ this.deviceConf[id][k] = intVal
191
+ }
192
+ break
193
+ }
194
+ case 'label':
195
+ case 'serialNumber':
196
+ this.deviceConf[id][k] = v
197
+ break
198
+ case 'listenerType':
199
+ case 'showAs': {
200
+ const inSet = platformConsts.allowed[k].includes(v)
201
+ if (typeof v !== 'string' || !inSet) {
202
+ logIgnore(`${key}.${id}.${k}`)
203
+ }
204
+ this.deviceConf[id][k] = inSet ? v : platformConsts.defaultValues[k]
205
+ break
206
+ }
207
+ case 'makerType':
208
+ this.deviceConf[id].showAsGarage = x[k].toString() === 'garageDoor'
209
+ break
210
+ case 'manualIP':
211
+ if (typeof v !== 'string' || v === '') {
212
+ logIgnore(`${key}.${id}.${k}`)
213
+ } else {
214
+ const manualIp = v
215
+ .toString()
216
+ .toLowerCase()
217
+ .replace(/[\s'"]+/g, '')
218
+ this.manualDevices.push(manualIp)
219
+ }
220
+ break
221
+ case 'enableColourControl':
222
+ case 'outletInUseTrue':
223
+ case 'reversePolarity':
224
+ case 'showTodayTC':
225
+ if (typeof v === 'string') {
226
+ logQuotes(`${key}.${id}.${k}`)
227
+ }
228
+ this.deviceConf[id][k] = v === 'false' ? false : !!v
229
+ break
230
+ case 'ignoreDevice':
231
+ if (typeof v === 'string') {
232
+ logQuotes(`${key}.${id}.${k}`)
233
+ }
234
+ if (!!v && v !== 'false') {
235
+ this.ignoredDevices.push(id)
236
+ }
237
+ break
238
+ default:
239
+ }
240
+ })
241
+ })
242
+ } else {
243
+ logIgnore(key)
244
+ }
245
+ break
246
+ case 'mode': {
247
+ const inSet = platformConsts.allowed[key].includes(val)
248
+ if (typeof val !== 'string' || !inSet) {
249
+ logIgnore(key)
250
+ }
251
+ this.config.mode = inSet ? val : 'auto'
252
+ break
253
+ }
254
+ case 'name':
255
+ case 'platform':
256
+ break
257
+ case 'removeByName':
258
+ if (typeof val !== 'string' || val === '') {
259
+ logIgnore(key)
260
+ }
261
+ this.config.removeByName = val
262
+ break
263
+ case 'wemoClient':
264
+ if (typeof val === 'object' && Object.keys(val).length > 0) {
265
+ Object.entries(val).forEach((subEntry) => {
266
+ const [k1, v1] = subEntry
267
+ switch (k1) {
268
+ case 'callback_url':
269
+ if (typeof v1 !== 'string' || v1 === '') {
270
+ logIgnore(`${key}.${k1}`)
271
+ }
272
+ this.config.callbackOverride = v1.replace('http://', '').replace(/\/\s*$/, '')
273
+ break
274
+ case 'discover_opts':
275
+ if (typeof v1 === 'object' && Object.keys(v1).length > 0) {
276
+ Object.entries(v1).forEach((subSubEntry) => {
277
+ const [k2, v2] = subSubEntry
278
+ switch (k2) {
279
+ case 'explicitSocketBind':
280
+ if (typeof v2 === 'string') {
281
+ logQuotes(`${key}.${k1}.${k2}`)
282
+ }
283
+ this.config[key][k1][k2] = v2 === 'false' ? false : !!v2
284
+ break
285
+ case 'interfaces':
286
+ if (typeof v2 !== 'string' || v2 === '') {
287
+ logIgnore(`${key}.${k1}.${k2}`)
288
+ }
289
+ this.config[key][k1][k2] = v2.toString()
290
+ break
291
+ default:
292
+ logRemove(`${key}.${k1}.${k2}`)
293
+ break
294
+ }
295
+ })
296
+ } else {
297
+ logIgnore(`${key}.${k1}`)
298
+ }
299
+ break
300
+ case 'listen_interface':
301
+ if (typeof v1 !== 'string' || v1 === '') {
302
+ logIgnore(`${key}.${k1}`)
303
+ }
304
+ this.config[key][k1] = v1
305
+ break
306
+ case 'port': {
307
+ if (typeof val === 'string') {
308
+ logQuotes(`${key}.${k1}`)
309
+ }
310
+ const intVal = Number.parseInt(v1, 10)
311
+ if (Number.isNaN(intVal)) {
312
+ logDefault(`${key}.${k1}`, platformConsts.defaultValues[k1])
313
+ this.config[key][k1] = platformConsts.defaultValues[k1]
314
+ } else if (intVal < platformConsts.minValues[k1]) {
315
+ logIncrease(`${key}.${k1}`, platformConsts.minValues[k1])
316
+ this.config[key][k1] = platformConsts.minValues[k1]
317
+ } else {
318
+ this.config[key][k1] = intVal
319
+ }
320
+ break
321
+ }
322
+ default:
323
+ logRemove(`${key}.${k1}`)
324
+ break
325
+ }
326
+ })
327
+ } else {
328
+ logIgnore(key)
329
+ }
330
+ break
331
+ default:
332
+ logRemove(key)
333
+ break
334
+ }
335
+ })
336
+ }
337
+
338
+ pluginSetup() {
339
+ // Plugin has finished initialising so now onto setup
340
+ try {
341
+ // Log that the plugin initialisation has been successful
342
+ this.log('%s.', platformLang.initialised)
343
+
344
+ // Sort out some logging functions
345
+ if (this.isBeta) {
346
+ this.log.debug = this.log
347
+ this.log.debugWarn = this.log.warn
348
+
349
+ // Log that using a beta will generate a lot of debug logs
350
+ if (this.isBeta) {
351
+ const divide = '*'.repeat(platformLang.beta.length + 1) // don't forget the full stop (+1!)
352
+ this.log.warn(divide)
353
+ this.log.warn(`${platformLang.beta}.`)
354
+ this.log.warn(divide)
355
+ }
356
+ } else {
357
+ this.log.debug = () => {}
358
+ this.log.debugWarn = () => {}
359
+ }
360
+
361
+ // Set up the discovery run counter
362
+ this.discoveryRun = -1
363
+
364
+ // Require any libraries that the accessory instances use
365
+ this.eveChar = new eveChars(this.api)
366
+ this.eveService = eveService(this.api)
367
+
368
+ // Set up the http client
369
+ this.httpClient = new httpClient(this)
370
+
371
+ // Configure each accessory restored from the cache
372
+ devicesInHB.forEach((accessory) => {
373
+ // If it's in the 'ignore list' or the removeByName option then remove
374
+ if (
375
+ this.ignoredDevices.includes(accessory.context.serialNumber.toUpperCase())
376
+ || this.config.removeByName === accessory.displayName
377
+ || (this.config.mode === 'semi'
378
+ && !cacheSerialsToConnect.includes(accessory.context.serialNumber))
379
+ ) {
380
+ this.removeAccessory(accessory)
381
+ return
382
+ }
383
+
384
+ // Make the accessory show as 'No Response' until it has been discovered
385
+ const { services } = accessory
386
+ services.forEach((service) => {
387
+ let charToError
388
+ switch (service.constructor.name) {
389
+ case 'AirPurifier':
390
+ case 'HeaterCooler':
391
+ case 'HumidifierDehumidifier':
392
+ charToError = 'Active'
393
+ break
394
+ case 'GarageDoorOpener':
395
+ charToError = 'TargetDoorState'
396
+ break
397
+ case 'Lightbulb':
398
+ case 'Outlet':
399
+ case 'Switch':
400
+ charToError = 'On'
401
+ break
402
+ default:
403
+ return
404
+ }
405
+ service
406
+ .getCharacteristic(this.api.hap.Characteristic[charToError])
407
+ .onSet(() => {
408
+ this.log.warn('[%s] %s.', accessory.displayName, platformLang.accNotReady)
409
+ throw new this.api.hap.HapStatusError(-70402)
410
+ })
411
+ .updateValue(new this.api.hap.HapStatusError(-70402))
412
+ })
413
+
414
+ // Update the context that the accessory can't be controlled until discovered
415
+ accessory.context.initialised = false
416
+ accessory.context.httpOnline = false
417
+ accessory.context.upnpOnline = false
418
+
419
+ // Add the accessory to cache accessories to connect to if it isn't already
420
+ if (!cacheSerialsToConnect.includes(accessory.context.serialNumber)) {
421
+ cacheSerialsToConnect.push()
422
+ }
423
+
424
+ // Update the changes to the accessory to the platform
425
+ this.api.updatePlatformAccessories([accessory])
426
+ devicesInHB.set(accessory.UUID, accessory)
427
+ })
428
+
429
+ // Set up the listener server for device notifications
430
+ listenerServer = createServer((req, res) => {
431
+ let body = ''
432
+ const accessory = devicesInHB.get(req.url.substring(1))
433
+ if (req.method === 'NOTIFY' && accessory) {
434
+ // A notification from a known device
435
+ req.on('data', (chunk) => {
436
+ body += chunk.toString()
437
+ })
438
+ req.on('end', () => this.httpClient.receiveDeviceUpdate(accessory, body))
439
+ }
440
+ res.writeHead(200)
441
+ res.end()
442
+ })
443
+
444
+ // Start listening on the above created server
445
+ if (this.config.wemoClient.listen_interface) {
446
+ // User has defined a specific network interface to listen on
447
+ listenerServer.listen(this.config.wemoClient.port, this.getLocalInterfaceAddress(), (err) => {
448
+ if (err) {
449
+ this.log.warn('%s: %s.', platformLang.listenerError, err)
450
+ } else if (this.config.debug) {
451
+ // Log the port of the listener server in debug mode
452
+ this.log('%s [%s].', platformLang.listenerPort, listenerServer.address().port)
453
+ }
454
+ })
455
+ } else {
456
+ // User has not defined a specific network interface to listen on
457
+ listenerServer.listen(this.config.wemoClient.port, (err) => {
458
+ if (err) {
459
+ this.log.warn('%s: %s', platformLang.listenerError, err)
460
+ } else if (this.config.debug) {
461
+ // Log the port of the listener server in debug mode
462
+ this.log('%s [%s].', platformLang.listenerPort, listenerServer.address().port)
463
+ }
464
+ })
465
+ }
466
+
467
+ // Set up the SSDP client if the user has not specified manual devices only
468
+ if (this.config.mode !== 'manual') {
469
+ if (this.isBeta) {
470
+ this.config.wemoClient.discover_opts.customLogger = this.log
471
+ }
472
+ ssdpClient = new ssdp(this.config.wemoClient.discover_opts)
473
+ }
474
+
475
+ // Setup successful
476
+ this.log('%s. %s', platformLang.complete, platformLang.welcome)
477
+
478
+ // Perform the first discovery run and set up the interval for subsequent runs
479
+ this.discoverDevices()
480
+ this.refreshInterval = setInterval(
481
+ () => this.discoverDevices(),
482
+ this.config.discoveryInterval * 1000,
483
+ )
484
+ } catch (err) {
485
+ // Catch any errors during setup
486
+ this.log.warn('***** %s. *****', platformLang.disabling)
487
+ this.log.warn('***** %s. *****', parseError(err))
488
+ this.pluginShutdown()
489
+ }
490
+ }
491
+
492
+ pluginShutdown() {
493
+ try {
494
+ // Stop the discovery interval if it's running
495
+ if (this.refreshInterval) {
496
+ clearInterval(this.refreshInterval)
497
+ }
498
+
499
+ // Shutdown the listener server if it's running
500
+ if (listenerServer) {
501
+ listenerServer.close(() => {
502
+ if (this.config.debug) {
503
+ this.log('%s.', platformLang.listenerClosed)
504
+ }
505
+ })
506
+ }
507
+
508
+ // Stop the SSDP client if it's running
509
+ if (ssdpClient) {
510
+ ssdpClient.stop()
511
+ if (this.config.debug) {
512
+ this.log('%s.', platformLang.ssdpStopped)
513
+ }
514
+ }
515
+
516
+ // Close accessory subscriptions
517
+ devicesInHB.forEach((accessory) => {
518
+ if (accessory.control?.pollingInterval) {
519
+ clearInterval(accessory.control.pollingInterval)
520
+ }
521
+ if (accessory.client) {
522
+ accessory.client.stopSubscriptions()
523
+ }
524
+ })
525
+ } catch (err) {
526
+ // No need to show errors at this point
527
+ }
528
+ }
529
+
530
+ async discoverDevices() {
531
+ // Increment the discovery run count
532
+ this.discoveryRun += 1
533
+ const accessoryArray = [...devicesInHB.values()]
534
+
535
+ // Nothing to do if mode is semi or manual and no device is re/awaiting connection
536
+ if (
537
+ this.config.mode !== 'auto'
538
+ && existSerialsToConnect.length === 0
539
+ && cacheSerialsToConnect.length === 0
540
+ ) {
541
+ return
542
+ }
543
+
544
+ // ********************* \\
545
+ // Auto Detected Devices \\
546
+ // ********************* \\
547
+ if (
548
+ this.config.mode === 'auto'
549
+ || (this.config.mode === 'semi'
550
+ && (cacheSerialsToConnect.length > 0 || existSerialsToConnect.length > 0))
551
+ ) {
552
+ // Remove all previous listeners as we don't want duplications on each interval
553
+ ssdpClient.removeAllListeners('response')
554
+
555
+ // Set up the listener for a detected device
556
+ ssdpClient.on('response', async (msg) => {
557
+ // Don't continue if it's not a Wemo device (service type)
558
+ if (msg.ST !== 'urn:Belkin:service:basicevent:1') {
559
+ return
560
+ }
561
+
562
+ // Get some information from the USN and location for checks
563
+ const urlParts = new url(msg.LOCATION)
564
+ const usnParts = msg.USN.split('::')
565
+ const deviceIP = urlParts.hostname
566
+ const devicePort = urlParts.port
567
+ const deviceUDN = usnParts[0]
568
+ try {
569
+ // Checks for if the device is manually configured or if the device is ignored
570
+ if (
571
+ this.manualDevices.some(el => el.includes(deviceIP))
572
+ || this.ignoredDevices.some(el => deviceUDN.toUpperCase().includes(el))
573
+ ) {
574
+ return
575
+ }
576
+
577
+ const deviceData = await this.getDeviceInfo(deviceIP, devicePort)
578
+
579
+ // Don't continue if we haven't found the correct port
580
+ if (!deviceData) {
581
+ throw new Error(platformLang.noPort)
582
+ }
583
+
584
+ // Don't continue specifically if the device type is switch: https://bit.ly/hb-pywemo-link
585
+ if (deviceData.deviceType === 'urn:Belkin:device:switch:1') {
586
+ return
587
+ }
588
+
589
+ // Don't continue if the mode is semi, and it's not a configured device
590
+ if (this.config.mode === 'semi' && !this.deviceConf[deviceData.serialNumber]) {
591
+ return
592
+ }
593
+
594
+ // Find a matching Homebridge accessory
595
+ const accessory = accessoryArray.find(el => el.context.udn === deviceUDN)
596
+
597
+ // Check if the accessory exists
598
+ if (accessory?.context?.initialised) {
599
+ if (
600
+ !existSerialsToConnect.includes(accessory.context.serialNumber)
601
+ || !accessory.client
602
+ ) {
603
+ // Accessory exists and client has reported no error, nothing to do
604
+ return
605
+ }
606
+ // Accessory exists but client has failed so renew
607
+ this.reinitialiseDevice(accessory, deviceData)
608
+ } else {
609
+ // Accessory does not exist in Homebridge
610
+ await this.initialiseDevice(deviceData)
611
+ }
612
+ } catch (err) {
613
+ // Show warnings on runs 0 (initial), 2, 5, 8, 11, ... just to limit logging to an extent
614
+ if (
615
+ !this.config.hideConnectionErrors
616
+ && (this.discoveryRun === 0 || this.discoveryRun % 3 === 2)
617
+ ) {
618
+ const eText = parseError(err, [platformLang.noPort])
619
+ this.log.warn('[%s] %s: %s.', deviceIP, platformLang.connError, eText)
620
+ }
621
+ }
622
+ })
623
+
624
+ // Perform the scan
625
+ try {
626
+ await ssdpClient.search('urn:Belkin:service:basicevent:1')
627
+ } catch (err) {
628
+ const eText = err.message === 'No sockets available, cannot start.'
629
+ ? platformLang.noSockets
630
+ : parseError(err)
631
+ this.log.warn('%s %s.', platformLang.ssdpFail, eText)
632
+ }
633
+ }
634
+
635
+ // *************************** \\
636
+ // Manually Configured Devices \\
637
+ // *************************** \\
638
+ this.manualDevices.forEach(async (device) => {
639
+ try {
640
+ // Check to see if the entry is a full address or an IP
641
+ let deviceIP
642
+ let devicePort
643
+ if (device.includes(':')) {
644
+ // It's a full address so get some information from the address
645
+ const urlParts = new url(device)
646
+ deviceIP = urlParts.hostname
647
+ devicePort = urlParts.port
648
+ } else {
649
+ // It's an IP so perform a port scan
650
+ deviceIP = device
651
+ devicePort = null
652
+ }
653
+ const deviceData = await this.getDeviceInfo(deviceIP, devicePort)
654
+
655
+ // Don't continue if no port was found
656
+ if (!deviceData) {
657
+ throw new Error(platformLang.noPort)
658
+ }
659
+
660
+ // Don't continue if the device is on the 'ignore list'
661
+ if (this.ignoredDevices.includes(deviceData.serialNumber.toUpperCase())) {
662
+ return
663
+ }
664
+
665
+ // Find a matching Homebridge accessory
666
+ const accessory = accessoryArray.find(el => el.context.udn === deviceData.UDN)
667
+
668
+ // Check if the accessory exists
669
+ if (accessory?.context?.initialised) {
670
+ if (
671
+ !existSerialsToConnect.includes(accessory.context.serialNumber)
672
+ || !accessory.client
673
+ ) {
674
+ // Accessory exists and client has reported no error, nothing to do
675
+ return
676
+ }
677
+ // Accessory exists but client has failed so renew
678
+ this.reinitialiseDevice(accessory, deviceData)
679
+ } else {
680
+ // Accessory does not exist in Homebridge
681
+ await this.initialiseDevice(deviceData)
682
+ }
683
+ } catch (err) {
684
+ // Show warnings on runs 0 (initial), 2, 5, 8, 11, ... just to limit logging to an extent
685
+ if (
686
+ !this.config.hideConnectionErrors
687
+ && (this.discoveryRun === 0 || this.discoveryRun % 3 === 2)
688
+ ) {
689
+ const eText = parseError(err, [platformLang.noPort])
690
+ this.log.warn('[%s] %s: %s.', device, platformLang.connError, eText)
691
+ }
692
+ }
693
+ })
694
+
695
+ // ************************ \\
696
+ // Erroneous Device Logging \\
697
+ // ************************ \\
698
+ if (this.discoveryRun % 3 === 2) {
699
+ // Add a small delay in case devices were discovered on this round
700
+ setTimeout(() => {
701
+ if (cacheSerialsToConnect.length > 0) {
702
+ const names = accessoryArray.filter(el => cacheSerialsToConnect.includes(el.context.serialNumber))
703
+ if (names.length > 0 && !this.config.hideConnectionErrors) {
704
+ this.log.warn(
705
+ '%s: [%s].',
706
+ platformLang.awaiting,
707
+ names.map(el => el.displayName).join('], ['),
708
+ )
709
+ }
710
+ }
711
+ }, 3000)
712
+ }
713
+
714
+ // Reset the discovery counter to 0
715
+ if (this.discoveryRun === 3) {
716
+ this.discoveryRun = 0
717
+ }
718
+ }
719
+
720
+ async getDeviceInfo(thisIp, portToTryFirst) {
721
+ // Try to find the correct port of a device by ip
722
+ // Credit to @Zacknetic for this function
723
+ const tryPort = async (port, ipAddress) => {
724
+ try {
725
+ // Send a request to the device URL to get the XML information
726
+ const res = await axios.get(`http://${ipAddress}:${port}/setup.xml`, {
727
+ timeout: 5000,
728
+ })
729
+
730
+ // Parse the XML response from the device
731
+ const json = await parseStringPromise(res.data, { explicitArray: false })
732
+ const { device } = json.root
733
+
734
+ // Add extra properties to the device variable
735
+ device.host = ipAddress
736
+ device.port = port
737
+ device.cbURL = this.config.callbackOverride
738
+ || `${this.getLocalInterfaceAddress(ipAddress)}:${listenerServer.address().port}`
739
+
740
+ // Return the XML2JS data
741
+ return device
742
+ } catch (err) {
743
+ // Suppress any errors as we don't want to show them
744
+ return false
745
+ }
746
+ }
747
+
748
+ // Loop through the ports that Wemo devices generally use
749
+ let portsToTry = platformConsts.portsToScan
750
+ if (portToTryFirst) {
751
+ portsToTry = portsToTry.filter(el => el !== portToTryFirst)
752
+ portsToTry.unshift(portToTryFirst)
753
+ }
754
+
755
+ for (const port of portsToTry) {
756
+ const portAttempt = await tryPort(port, thisIp)
757
+ if (portAttempt) {
758
+ // We found the correct port
759
+ return portAttempt
760
+ }
761
+ }
762
+
763
+ // None of the ports worked
764
+ return false
765
+ }
766
+
767
+ applyAccessoryLogging(accessory) {
768
+ if (this.isBeta) {
769
+ accessory.log = msg => this.log('[%s] %s.', accessory.displayName, msg)
770
+ accessory.logWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
771
+ accessory.logDebug = msg => this.log('[%s] %s.', accessory.displayName, msg)
772
+ accessory.logDebugWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
773
+ } else {
774
+ if (this.config.disableDeviceLogging) {
775
+ accessory.log = () => {}
776
+ accessory.logWarn = () => {}
777
+ } else {
778
+ accessory.log = msg => this.log('[%s] %s.', accessory.displayName, msg)
779
+ accessory.logWarn = msg => this.log.warn('[%s] %s.', accessory.displayName, msg)
780
+ }
781
+ accessory.logDebug = () => {}
782
+ accessory.logDebugWarn = () => {}
783
+ }
784
+ }
785
+
786
+ async initialiseDevice(device) {
787
+ try {
788
+ let accessory
789
+
790
+ // Generate the uuid for this device from the device UDN
791
+ const uuid = this.api.hap.uuid.generate(device.UDN)
792
+
793
+ // Remove the device from the pending connection list
794
+ cacheSerialsToConnect = cacheSerialsToConnect.filter(el => el !== device.serialNumber)
795
+
796
+ // Obtain any user configured entry for this device
797
+ const deviceConf = this.deviceConf[device.serialNumber] || {}
798
+
799
+ // Save context information for the plugin to use
800
+ const listenerType = deviceConf.listenerType === 'http' ? 'http' : 'upnp'
801
+ const disableUPNP = this.config.disableUPNP ? 'http' : 'upnp'
802
+ const context = {
803
+ connection: deviceConf.listenerType ? listenerType : disableUPNP,
804
+ cbURL: device.cbURL,
805
+ firmware: device.firmwareVersion,
806
+ hidden: false,
807
+ icon: device.iconList?.icon?.url || false,
808
+ ipAddress: device.host,
809
+ macAddress: device.macAddress ? device.macAddress.replace(/..\B/g, '$&:') : false,
810
+ port: device.port,
811
+ serialNumber: device.serialNumber,
812
+ udn: device.UDN,
813
+ }
814
+
815
+ // Create a map of device services
816
+ const services = {}
817
+ if (device.serviceList) {
818
+ if (!Array.isArray(device.serviceList.service)) {
819
+ device.serviceList.service = [device.serviceList.service]
820
+ }
821
+ // Put all the useful service info into the services object
822
+ device.serviceList.service.forEach((service) => {
823
+ services[service.serviceType] = {
824
+ serviceId: service.serviceId,
825
+ controlURL: service.controlURL,
826
+ eventSubURL: service.eventSubURL,
827
+ }
828
+ })
829
+ } else {
830
+ // Device has no services so is useless to Homebridge
831
+ if (devicesInHB.has(uuid)) {
832
+ this.removeAccessory(devicesInHB.get(uuid))
833
+ }
834
+ throw new Error(platformLang.noServices)
835
+ }
836
+
837
+ // Update the context with the service object
838
+ context.serviceList = { ...services }
839
+
840
+ // Get the correct device type instance
841
+ switch (device.deviceType) {
842
+ case 'urn:Belkin:device:bridge:1': {
843
+ /**
844
+ ***************
845
+ WEMO LINKS * BULBS
846
+ ****************
847
+ */
848
+ if (!context.serviceList['urn:Belkin:service:bridge:1']) {
849
+ throw new Error(platformLang.noService)
850
+ }
851
+
852
+ // Set up the main 'hidden' accessory for the Link
853
+ accessory = this.addAccessory(device, true, true)
854
+ this.applyAccessoryLogging(accessory)
855
+ accessory.context = { ...accessory.context, ...context, hidden: true }
856
+ if (Object.keys(context.serviceList).length > 0 && context.connection === 'upnp') {
857
+ accessory.client = new upnpClient(this, accessory)
858
+ }
859
+ accessory.control = new deviceTypes.deviceLinkHub(this, accessory, devicesInHB)
860
+
861
+ // Request a list of subdevices from the Wemo Link
862
+ const xml = xmlCreate('s:Envelope', {
863
+ version: '1.0',
864
+ encoding: 'utf-8',
865
+ allowEmpty: true,
866
+ })
867
+ .att('xmlns:s', 'http://schemas.xmlsoap.org/soap/envelope/')
868
+ .att('s:encodingStyle', 'http://schemas.xmlsoap.org/soap/encoding/')
869
+ .ele('s:Body')
870
+ .ele('u:GetEndDevices')
871
+ .att('xmlns:u', 'urn:Belkin:service:bridge:1')
872
+
873
+ // Send the request to the device
874
+ const res = await axios({
875
+ url: `http://${device.host}:${device.port}/upnp/control/bridge1`,
876
+ method: 'post',
877
+ headers: {
878
+ 'SOAPACTION': '"urn:Belkin:service:bridge:1#GetEndDevices"',
879
+ 'Content-Type': 'text/xml; charset="utf-8"',
880
+ },
881
+ data: xml.ele({ DevUDN: device.UDN, ReqListType: 'PAIRED_LIST' }).end(),
882
+ timeout: 10000,
883
+ })
884
+
885
+ // Parse the response from the device
886
+ const xmlRes = res.data
887
+ const response = await parseStringPromise(xmlRes, { explicitArray: false })
888
+
889
+ // Get the data we need the parsed response
890
+ const data = response['s:Envelope']['s:Body']['u:GetEndDevicesResponse']
891
+
892
+ // Parse the XML response from the Wemo Link
893
+ const result = await parseStringPromise(data.DeviceLists)
894
+
895
+ // A function used later for parsing the device information
896
+ const parseDeviceInfo = (thisData) => {
897
+ const thisDevice = {}
898
+ if (thisData.GroupID) {
899
+ // Treat device group as if it were a single device
900
+ thisDevice.friendlyName = thisData.GroupName[0]
901
+ thisDevice.deviceId = thisData.GroupID[0]
902
+ const values = thisData.GroupCapabilityValues[0].split(',')
903
+ thisDevice.capabilities = {}
904
+ thisData.GroupCapabilityIDs[0].split(',').forEach((val, index) => {
905
+ thisDevice.capabilities[val] = values[index]
906
+ })
907
+ } else {
908
+ // Single device
909
+ thisDevice.friendlyName = thisData.FriendlyName[0]
910
+ thisDevice.deviceId = thisData.DeviceID[0]
911
+ const values = thisData.CurrentState[0].split(',')
912
+ thisDevice.capabilities = {}
913
+ thisData.CapabilityIDs[0].split(',').forEach((val, index) => {
914
+ thisDevice.capabilities[val] = values[index]
915
+ })
916
+ }
917
+ return thisDevice
918
+ }
919
+
920
+ // Create an array of subdevices we can use
921
+ const subdevices = []
922
+ const deviceInfos = result.DeviceLists.DeviceList[0].DeviceInfos[0].DeviceInfo
923
+ if (deviceInfos) {
924
+ Array.prototype.push.apply(subdevices, deviceInfos.map(parseDeviceInfo))
925
+ }
926
+ if (result.DeviceLists.DeviceList[0].GroupInfos) {
927
+ const groupInfos = result.DeviceLists.DeviceList[0].GroupInfos[0].GroupInfo
928
+ Array.prototype.push.apply(subdevices, groupInfos.map(parseDeviceInfo))
929
+ }
930
+
931
+ // Loop through the subdevices initialising each one
932
+ subdevices.forEach((subdevice) => {
933
+ try {
934
+ // Don't continue if the device is on the 'ignore list'
935
+ if (this.ignoredDevices.includes(subdevice.deviceId.toUpperCase())) {
936
+ return
937
+ }
938
+
939
+ // Give the subdevice some extra context (primary, secondary serial numbers)
940
+ const extraContext = {
941
+ capabilities: subdevice.capabilities,
942
+ linkSerialNumber: device.serialNumber,
943
+ serialNumber: subdevice.deviceId,
944
+ }
945
+
946
+ // Generate the uuid for this subdevice from the subdevice id
947
+ const uuidSub = this.api.hap.uuid.generate(subdevice.deviceId)
948
+
949
+ // Get the cached accessory or add to Homebridge if it doesn't exist
950
+ const subAcc = devicesInHB.get(uuidSub) || this.addAccessory(subdevice)
951
+ subAcc.context = { ...subAcc.context, ...context, ...extraContext }
952
+ this.applyAccessoryLogging(subAcc)
953
+ subAcc.control = new deviceTypes.deviceLinkBulb(this, accessory, subAcc)
954
+
955
+ // Log the successfully initialised device
956
+ this.log(
957
+ '[%s] %s %s %s %s:%s.',
958
+ subAcc.displayName,
959
+ platformLang.initSer,
960
+ subdevice.deviceId,
961
+ platformLang.initMac,
962
+ subAcc.context.ipAddress,
963
+ subAcc.context.port,
964
+ )
965
+
966
+ // Mark the device as initialised and the http status as online
967
+ subAcc.context.initialised = true
968
+ subAcc.context.httpOnline = true
969
+ if (context.connection === 'upnp') {
970
+ subAcc.context.upnpOnline = true
971
+ }
972
+
973
+ // Update any changes to the accessory to the platform
974
+ this.api.updatePlatformAccessories([subAcc])
975
+ devicesInHB.set(uuidSub, subAcc)
976
+ } catch (err) {
977
+ // Catch any errors during the process
978
+ const eText = parseError(err)
979
+ this.log.warn('[%s] %s %s.', subdevice.friendlyName, platformLang.devNotInit, eText)
980
+ }
981
+ })
982
+ break
983
+ /** */
984
+ }
985
+ case 'urn:Belkin:device:insight:1': {
986
+ /**
987
+ **********
988
+ WEMO INSIGHTS
989
+ ***********
990
+ */
991
+ // Retrieve or add accessory, and add client and control properties
992
+ accessory = devicesInHB.get(uuid) || this.addAccessory(device, true)
993
+ accessory.context = { ...accessory.context, ...context }
994
+ this.applyAccessoryLogging(accessory)
995
+ if (Object.keys(context.serviceList).length > 0 && context.connection === 'upnp') {
996
+ accessory.client = new upnpClient(this, accessory)
997
+ }
998
+ const showAs = deviceConf.showAs || platformConsts.defaultValues.showAs
999
+ switch (showAs) {
1000
+ case 'purifier':
1001
+ accessory.control = new deviceTypes.deviceSimPurifierInsight(this, accessory)
1002
+ break
1003
+ case 'switch':
1004
+ accessory.control = new deviceTypes.deviceSimSwitchInsight(this, accessory)
1005
+ break
1006
+ default:
1007
+ accessory.control = new deviceTypes.deviceInsight(this, accessory)
1008
+ break
1009
+ }
1010
+ break
1011
+ /** */
1012
+ }
1013
+ case 'urn:Belkin:device:dimmer:1': {
1014
+ /**
1015
+ *********
1016
+ WEMO DIMMERS
1017
+ **********
1018
+ */
1019
+ accessory = devicesInHB.get(uuid) || this.addAccessory(device, true)
1020
+ accessory.context = { ...accessory.context, ...context }
1021
+ this.applyAccessoryLogging(accessory)
1022
+ if (Object.keys(context.serviceList).length > 0 && context.connection === 'upnp') {
1023
+ accessory.client = new upnpClient(this, accessory)
1024
+ }
1025
+ accessory.control = new deviceTypes.deviceDimmer(this, accessory)
1026
+ break
1027
+ /** */
1028
+ }
1029
+ case 'urn:Belkin:device:lightswitch:1': {
1030
+ /**
1031
+ ****************
1032
+ WEMO LIGHT SWITCHES
1033
+ *****************
1034
+ */
1035
+ accessory = devicesInHB.get(uuid) || this.addAccessory(device, true)
1036
+ accessory.context = { ...accessory.context, ...context }
1037
+ this.applyAccessoryLogging(accessory)
1038
+ if (Object.keys(context.serviceList).length > 0 && context.connection === 'upnp') {
1039
+ accessory.client = new upnpClient(this, accessory)
1040
+ }
1041
+ accessory.control = new deviceTypes.deviceLightSwitch(this, accessory)
1042
+ break
1043
+ /** */
1044
+ }
1045
+ case 'urn:Belkin:device:Maker:1': {
1046
+ /**
1047
+ ********
1048
+ WEMO MAKERS
1049
+ *********
1050
+ */
1051
+ // Retrieve or add accessory, and add client and control properties
1052
+ accessory = devicesInHB.get(uuid) || this.addAccessory(device, true)
1053
+ accessory.context = { ...accessory.context, ...context }
1054
+ this.applyAccessoryLogging(accessory)
1055
+ if (Object.keys(context.serviceList).length > 0 && context.connection === 'upnp') {
1056
+ accessory.client = new upnpClient(this, accessory)
1057
+ }
1058
+ if (deviceConf.showAsGarage) {
1059
+ accessory.control = new deviceTypes.deviceMakerGarage(this, accessory)
1060
+ } else {
1061
+ accessory.control = new deviceTypes.deviceMakerSwitch(this, accessory)
1062
+ }
1063
+ break
1064
+ /** */
1065
+ }
1066
+ case 'urn:Belkin:device:sensor:1':
1067
+ case 'urn:Belkin:device:NetCamSensor:1': {
1068
+ /**
1069
+ *********
1070
+ WEMO MOTIONS
1071
+ **********
1072
+ */
1073
+ accessory = devicesInHB.get(uuid) || this.addAccessory(device, true)
1074
+ accessory.context = { ...accessory.context, ...context }
1075
+ this.applyAccessoryLogging(accessory)
1076
+ if (Object.keys(context.serviceList).length > 0 && context.connection === 'upnp') {
1077
+ accessory.client = new upnpClient(this, accessory)
1078
+ }
1079
+ accessory.control = new deviceTypes.deviceMotion(this, accessory)
1080
+ break
1081
+ /** */
1082
+ }
1083
+ case 'urn:Belkin:device:controllee:1':
1084
+ case 'urn:Belkin:device:outdoor:1': {
1085
+ /**
1086
+ **********
1087
+ WEMO SWITCHES
1088
+ ***********
1089
+ */
1090
+ // Retrieve or add accessory, and add client and control properties
1091
+ accessory = devicesInHB.get(uuid) || this.addAccessory(device, true)
1092
+ accessory.context = { ...accessory.context, ...context }
1093
+ this.applyAccessoryLogging(accessory)
1094
+ if (Object.keys(context.serviceList).length > 0 && context.connection === 'upnp') {
1095
+ accessory.client = new upnpClient(this, accessory)
1096
+ }
1097
+
1098
+ const showAs = deviceConf.showAs || platformConsts.defaultValues.showAs
1099
+ switch (showAs) {
1100
+ case 'purifier':
1101
+ accessory.control = new deviceTypes.deviceSimPurifier(this, accessory)
1102
+ break
1103
+ case 'switch':
1104
+ accessory.control = new deviceTypes.deviceSimSwitch(this, accessory)
1105
+ break
1106
+ default:
1107
+ accessory.control = new deviceTypes.deviceOutlet(this, accessory)
1108
+ break
1109
+ }
1110
+ break
1111
+ /** */
1112
+ }
1113
+ case 'urn:Belkin:device:HeaterA:1':
1114
+ case 'urn:Belkin:device:HeaterB:1': {
1115
+ /**
1116
+ *********
1117
+ WEMO HEATERS
1118
+ **********
1119
+ */
1120
+ accessory = devicesInHB.get(uuid) || this.addAccessory(device, true)
1121
+ accessory.context = { ...accessory.context, ...context }
1122
+ this.applyAccessoryLogging(accessory)
1123
+ if (Object.keys(context.serviceList).length > 0 && context.connection === 'upnp') {
1124
+ accessory.client = new upnpClient(this, accessory)
1125
+ }
1126
+ accessory.control = new deviceTypes.deviceHeater(this, accessory)
1127
+ break
1128
+ /** */
1129
+ }
1130
+ case 'urn:Belkin:device:Humidifier:1':
1131
+ case 'urn:Belkin:device:HumidifierB:1': {
1132
+ /**
1133
+ *************
1134
+ WEMO HUMIDIFIERS
1135
+ **************
1136
+ */
1137
+ accessory = devicesInHB.get(uuid) || this.addAccessory(device, true)
1138
+ accessory.context = { ...accessory.context, ...context }
1139
+ this.applyAccessoryLogging(accessory)
1140
+ if (Object.keys(context.serviceList).length > 0 && context.connection === 'upnp') {
1141
+ accessory.client = new upnpClient(this, accessory)
1142
+ }
1143
+ accessory.control = new deviceTypes.deviceHumidifier(this, accessory)
1144
+ break
1145
+ /** */
1146
+ }
1147
+ case 'urn:Belkin:device:AirPurifier:1': {
1148
+ /**
1149
+ ***********
1150
+ WEMO PURIFIERS
1151
+ ************
1152
+ */
1153
+ if (deviceConf.label) {
1154
+ device.friendlyName = deviceConf.label
1155
+ }
1156
+ accessory = devicesInHB.get(uuid) || this.addAccessory(device, true)
1157
+ accessory.context = { ...accessory.context, ...context }
1158
+ this.applyAccessoryLogging(accessory)
1159
+ if (Object.keys(context.serviceList).length > 0 && context.connection === 'upnp') {
1160
+ accessory.client = new upnpClient(this, accessory)
1161
+ }
1162
+ accessory.control = new deviceTypes.devicePurifier(this, accessory)
1163
+ break
1164
+ /** */
1165
+ }
1166
+ case 'urn:Belkin:device:crockpot:1': {
1167
+ /**
1168
+ ***********
1169
+ WEMO CROCKPOTS
1170
+ ************
1171
+ */
1172
+ accessory = devicesInHB.get(uuid) || this.addAccessory(device, true)
1173
+
1174
+ // Wemo Crockpot does not support UPnP so override the connection to http
1175
+ accessory.context = { ...accessory.context, ...context, connection: 'http' }
1176
+ this.applyAccessoryLogging(accessory)
1177
+ if (Object.keys(context.serviceList).length > 0 && context.connection === 'upnp') {
1178
+ accessory.client = new upnpClient(this, accessory)
1179
+ }
1180
+ accessory.control = new deviceTypes.deviceCrockpot(this, accessory)
1181
+ break
1182
+ /** */
1183
+ }
1184
+ case 'urn:Belkin:device:CoffeeMaker:1': {
1185
+ /**
1186
+ ***************
1187
+ WEMO COFFEE MAKERS
1188
+ ****************
1189
+ */
1190
+ accessory = devicesInHB.get(uuid) || this.addAccessory(device, true)
1191
+ accessory.context = { ...accessory.context, ...context }
1192
+ this.applyAccessoryLogging(accessory)
1193
+ if (Object.keys(context.serviceList).length > 0 && context.connection === 'upnp') {
1194
+ accessory.client = new upnpClient(this, accessory)
1195
+ }
1196
+ accessory.control = new deviceTypes.deviceCoffee(this, accessory)
1197
+ break
1198
+ /** */
1199
+ }
1200
+ default: {
1201
+ /**
1202
+ **************
1203
+ UNSUPPORTED AS YET
1204
+ ***************
1205
+ */
1206
+ this.log.warn(
1207
+ '[%s] [%s] %s.',
1208
+ device.friendlyName,
1209
+ device.deviceType,
1210
+ platformLang.unsupported,
1211
+ )
1212
+
1213
+ // Add device to ignore list to stop repeated notifications
1214
+ this.ignoredDevices.push(device.serialNumber.toUpperCase())
1215
+ return
1216
+ /** */
1217
+ }
1218
+ }
1219
+
1220
+ // Log the successfully initialised device
1221
+ this.log(
1222
+ '[%s] %s %s %s %s:%s',
1223
+ accessory.displayName,
1224
+ platformLang.initSer,
1225
+ device.serialNumber,
1226
+ platformLang.initMac,
1227
+ accessory.context.ipAddress,
1228
+ accessory.context.port,
1229
+ )
1230
+
1231
+ // Mark the device as initialised and the http status as online
1232
+ accessory.context.initialised = true
1233
+ accessory.context.httpOnline = true
1234
+ accessory.log(platformLang.httpGood)
1235
+
1236
+ // If upnp is enabled then start the subscriptions as mark the upnp status as online
1237
+ if (accessory.client) {
1238
+ accessory.client.startSubscriptions()
1239
+ accessory.context.upnpOnline = true
1240
+ accessory.log(platformLang.upnpGood)
1241
+ }
1242
+
1243
+ // Update any changes to the accessory to the platform
1244
+ this.api.updatePlatformAccessories([accessory])
1245
+ devicesInHB.set(uuid, accessory)
1246
+ } catch (err) {
1247
+ // Catch any errors during the process
1248
+ this.log.warn('[%s] %s %s.', device.friendlyName, platformLang.devNotInit, parseError(err))
1249
+ }
1250
+ }
1251
+
1252
+ addAccessory(device, isPri, hidden = false) {
1253
+ const accName = device.friendlyName || device.deviceId || device.serialNumber
1254
+ try {
1255
+ // Add an accessory to Homebridge
1256
+ const newUUID = this.api.hap.uuid.generate(isPri ? device.UDN : device.deviceId)
1257
+ const accessory = new this.api.platformAccessory(accName, newUUID)
1258
+
1259
+ // If it isn't a hidden device then set the accessory characteristics
1260
+ if (!hidden) {
1261
+ accessory
1262
+ .getService(this.api.hap.Service.AccessoryInformation)
1263
+ .setCharacteristic(this.api.hap.Characteristic.Name, accName)
1264
+ .setCharacteristic(this.api.hap.Characteristic.ConfiguredName, accName)
1265
+ .setCharacteristic(this.api.hap.Characteristic.Manufacturer, platformLang.brand)
1266
+ .setCharacteristic(
1267
+ this.api.hap.Characteristic.Model,
1268
+ isPri ? device.modelName : platformLang.modelLED,
1269
+ )
1270
+ .setCharacteristic(
1271
+ this.api.hap.Characteristic.SerialNumber,
1272
+ isPri ? device.serialNumber : device.deviceId,
1273
+ )
1274
+ .setCharacteristic(this.api.hap.Characteristic.Identify, true)
1275
+
1276
+ // Register the accessory if it hasn't been hidden by the user
1277
+ this.api.registerPlatformAccessories(plugin.name, plugin.alias, [accessory])
1278
+ this.log('[%s] %s.', accName, platformLang.devAdd)
1279
+ }
1280
+ this.configureAccessory(accessory)
1281
+ return accessory
1282
+ } catch (err) {
1283
+ // Catch any errors during add
1284
+ this.log.warn('[%s] %s %s.', accName, platformLang.devNotAdd, parseError(err))
1285
+ return false
1286
+ }
1287
+ }
1288
+
1289
+ configureAccessory(accessory) {
1290
+ // Add the configured accessory to our global map
1291
+ devicesInHB.set(accessory.UUID, accessory)
1292
+ }
1293
+
1294
+ removeAccessory(accessory) {
1295
+ try {
1296
+ // Remove an accessory from Homebridge
1297
+ if (!accessory.context.hidden) {
1298
+ this.api.unregisterPlatformAccessories(plugin.name, plugin.alias, [accessory])
1299
+ }
1300
+ devicesInHB.delete(accessory.UUID)
1301
+ this.log('[%s] %s.', accessory.displayName, platformLang.devRemove)
1302
+ } catch (err) {
1303
+ // Catch any errors during remove
1304
+ this.log.warn('[%s] %s %s.', accessory.displayName, platformLang.devNotRemove, parseError(err))
1305
+ }
1306
+ }
1307
+
1308
+ reinitialiseDevice(accessory, deviceData) {
1309
+ // Update the context with the new information
1310
+ accessory.context = {
1311
+ ...accessory.context,
1312
+ cbURL: deviceData.cbURL,
1313
+ controllable: true,
1314
+ ipAddress: deviceData.host,
1315
+ port: deviceData.port,
1316
+ }
1317
+
1318
+ // Mark the http status as online
1319
+ accessory.context.httpOnline = true
1320
+ accessory.log(platformLang.httpGood)
1321
+
1322
+ // If upnp is supported then restart subscriptions and mark the upnp status as online
1323
+ if (accessory.client) {
1324
+ accessory.client.startSubscriptions()
1325
+ accessory.context.upnpOnline = true
1326
+ accessory.log(platformLang.upnpGood)
1327
+ }
1328
+
1329
+ // Remove the accessory from the pending connection list
1330
+ existSerialsToConnect = existSerialsToConnect.filter(el => el !== deviceData.serialNumber)
1331
+ this.api.updatePlatformAccessories([accessory])
1332
+ devicesInHB.set(accessory.UUID, accessory)
1333
+ }
1334
+
1335
+ disableUPNP(accessory, err) {
1336
+ // Log the error immediately
1337
+ if (accessory.context.enableLogging) {
1338
+ accessory.logWarn(`${platformLang.upnpFail} [${err.message}]`)
1339
+ }
1340
+
1341
+ // Update the context now the device is uncontrollable
1342
+ accessory.context.upnpOnline = false
1343
+ existSerialsToConnect.push(accessory.context.serialNumber)
1344
+
1345
+ // Update any changes to the accessory to the platform
1346
+ this.api.updatePlatformAccessories([accessory])
1347
+ devicesInHB.set(accessory.UUID, accessory)
1348
+
1349
+ // Perform a http poll to see if the device is still reachable
1350
+ if (accessory.control?.requestDeviceUpdate) {
1351
+ // Above checks as example the wemo motion does not have this function
1352
+ accessory.control.requestDeviceUpdate()
1353
+ }
1354
+ }
1355
+
1356
+ updateHTTPStatus(accessory, newStatus) {
1357
+ if (newStatus) {
1358
+ // Mark the http status as online
1359
+ accessory.log(platformLang.httpGood)
1360
+ accessory.context.httpOnline = true
1361
+
1362
+ // If upnp is disabled then remove the device from the needs to be reinitialised
1363
+ if (accessory.context.connection === 'http') {
1364
+ existSerialsToConnect = existSerialsToConnect.filter(
1365
+ el => el !== accessory.context.serialNumber,
1366
+ )
1367
+ }
1368
+ } else {
1369
+ // Mark the http status as offline
1370
+ accessory.logWarn(platformLang.httpFail)
1371
+ accessory.context.httpOnline = false
1372
+
1373
+ // If upnp is disabled then mark the accessory as needs to be reinitialised
1374
+ if (accessory.context.connection === 'http') {
1375
+ existSerialsToConnect.push(accessory.context.serialNumber)
1376
+ }
1377
+ }
1378
+
1379
+ // Update any changes to the accessory to the platform
1380
+ this.api.updatePlatformAccessories([accessory])
1381
+ devicesInHB.set(accessory.UUID, accessory)
1382
+ }
1383
+
1384
+ getLocalInterfaceAddress(targetNetwork) {
1385
+ // Get a list of available network interfaces
1386
+ let interfaces = networkInterfaces()
1387
+
1388
+ // Check if the user has specified a network interface to listen on
1389
+ if (this.config.wemoClient.listen_interface) {
1390
+ // Specific interface in config, so check it exists in list
1391
+ if (interfaces[this.config.wemoClient.listen_interface]) {
1392
+ // Filter the interfaces object down to the specific interface
1393
+ interfaces = [interfaces[this.config.wemoClient.listen_interface]]
1394
+ } else {
1395
+ // Specified interface doesn't exist
1396
+ throw new Error(
1397
+ `${platformLang.noInterface} [${this.config.wemoClient.listen_interface}]`,
1398
+ )
1399
+ }
1400
+ }
1401
+
1402
+ // Get an appropriate IP address for the system
1403
+ const addresses = []
1404
+ Object.entries(interfaces).forEach((entry) => {
1405
+ const [k] = entry
1406
+ Object.entries(interfaces[k]).forEach((subEntry) => {
1407
+ const [k2] = subEntry
1408
+ const address = interfaces[k][k2]
1409
+ if ((address.family === 'IPv4' || address.family === 4) && !address.internal) {
1410
+ if (targetNetwork && ip.subnet(address.address, address.netmask).contains(targetNetwork)) {
1411
+ // Try to find IP address on the same IP network as the device's location
1412
+ addresses.unshift(address.address)
1413
+ } else {
1414
+ addresses.push(address.address)
1415
+ }
1416
+ }
1417
+ })
1418
+ })
1419
+
1420
+ // Return the IP address
1421
+ return addresses.shift()
1422
+ }
1423
+ }