@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.
- package/CHANGELOG.md +809 -0
- package/LICENSE +21 -0
- package/README.md +66 -0
- package/config.schema.json +814 -0
- package/eslint.config.js +50 -0
- package/lib/connection/http.js +174 -0
- package/lib/connection/upnp.js +155 -0
- package/lib/device/coffee.js +167 -0
- package/lib/device/crockpot.js +380 -0
- package/lib/device/dimmer.js +280 -0
- package/lib/device/heater.js +416 -0
- package/lib/device/humidifier.js +379 -0
- package/lib/device/index.js +39 -0
- package/lib/device/insight.js +353 -0
- package/lib/device/lightswitch.js +154 -0
- package/lib/device/link-bulb.js +467 -0
- package/lib/device/link-hub.js +63 -0
- package/lib/device/maker-garage.js +426 -0
- package/lib/device/maker-switch.js +202 -0
- package/lib/device/motion.js +148 -0
- package/lib/device/outlet.js +148 -0
- package/lib/device/purifier.js +468 -0
- package/lib/device/simulation/purifier-insight.js +357 -0
- package/lib/device/simulation/purifier.js +159 -0
- package/lib/device/simulation/switch-insight.js +315 -0
- package/lib/device/simulation/switch.js +154 -0
- package/lib/fakegato/LICENSE +21 -0
- package/lib/fakegato/fakegato-history.js +814 -0
- package/lib/fakegato/fakegato-storage.js +108 -0
- package/lib/fakegato/fakegato-timer.js +125 -0
- package/lib/fakegato/uuid.js +27 -0
- package/lib/homebridge-ui/public/index.html +315 -0
- package/lib/homebridge-ui/server.js +10 -0
- package/lib/index.js +8 -0
- package/lib/platform.js +1423 -0
- package/lib/utils/colour.js +135 -0
- package/lib/utils/constants.js +129 -0
- package/lib/utils/eve-chars.js +130 -0
- package/lib/utils/functions.js +52 -0
- package/lib/utils/lang-en.js +125 -0
- package/package.json +70 -0
package/lib/platform.js
ADDED
|
@@ -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
|
+
}
|