@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,468 @@
1
+ import { Builder, parseStringPromise } from 'xml2js'
2
+
3
+ import {
4
+ decodeXML,
5
+ generateRandomString,
6
+ parseError,
7
+ sleep,
8
+ } from '../utils/functions.js'
9
+ import platformLang from '../utils/lang-en.js'
10
+
11
+ export default class {
12
+ constructor(platform, accessory) {
13
+ // Set up variables from the platform
14
+ this.hapChar = platform.api.hap.Characteristic
15
+ this.hapErr = platform.api.hap.HapStatusError
16
+ this.hapServ = platform.api.hap.Service
17
+ this.platform = platform
18
+
19
+ // Set up variables from the accessory
20
+ this.accessory = accessory
21
+
22
+ // Add the purifier service if it doesn't already exist
23
+ this.service = this.accessory.getService(this.hapServ.AirPurifier)
24
+ || this.accessory.addService(this.hapServ.AirPurifier)
25
+
26
+ // Add the air quality service if it doesn't already exist
27
+ this.airService = this.accessory.getService(this.hapServ.AirQualitySensor)
28
+ || this.accessory.addService(this.hapServ.AirQualitySensor, 'Air Quality', 'airquality')
29
+
30
+ // Add the (ionizer) switch service if it doesn't already exist
31
+ this.ioService = this.accessory.getService(this.hapServ.Switch)
32
+ || this.accessory.addService(this.hapServ.Switch, 'Ionizer', 'ionizer')
33
+
34
+ // Add the set handler to the purifier active characteristic
35
+ this.service
36
+ .getCharacteristic(this.hapChar.Active)
37
+ .removeOnSet()
38
+ .onSet(async value => this.internalStateUpdate(value))
39
+
40
+ // Add options to the purifier target state characteristic
41
+ this.service
42
+ .getCharacteristic(this.hapChar.TargetAirPurifierState)
43
+ .updateValue(1)
44
+ .setProps({
45
+ minValue: 1,
46
+ maxValue: 1,
47
+ validValues: [1],
48
+ })
49
+
50
+ // Add the set handler to the purifier rotation speed (for mode) characteristic
51
+ this.service
52
+ .getCharacteristic(this.hapChar.RotationSpeed)
53
+ .setProps({ minStep: 25 })
54
+ .onSet(async (value) => {
55
+ await this.internalModeUpdate(value)
56
+ })
57
+
58
+ // Add the FilterChangeIndication characteristic to the air purifier if it isn't already
59
+ if (!this.service.testCharacteristic(this.hapChar.FilterChangeIndication)) {
60
+ this.service.addCharacteristic(this.hapChar.FilterChangeIndication)
61
+ }
62
+ this.cacheFilterX = this.service.getCharacteristic(this.hapChar.FilterChangeIndication).value
63
+
64
+ // Add the FilterLifeLevel characteristic to the air purifier if it isn't already
65
+ if (!this.service.testCharacteristic(this.hapChar.FilterLifeLevel)) {
66
+ this.service.addCharacteristic(this.hapChar.FilterLifeLevel)
67
+ }
68
+ this.cacheFilter = this.service.getCharacteristic(this.hapChar.FilterLifeLevel).value
69
+
70
+ // Add the set handler to the switch (for ionizer) characteristic
71
+ this.ioService.getCharacteristic(this.hapChar.On).onSet(async (value) => {
72
+ await this.internalIonizerUpdate(value)
73
+ })
74
+
75
+ // Add a last mode cache value if not already set
76
+ if (![1, 2, 3, 4].includes(this.accessory.context.cacheLastOnMode)) {
77
+ this.accessory.context.cacheLastOnMode = 1
78
+ }
79
+
80
+ // Add an ionizer on/off cache value if not already set
81
+ if (![0, 1].includes(this.accessory.context.cacheIonizerOn)) {
82
+ this.accessory.context.cacheIonizerOn = 0
83
+ }
84
+
85
+ // Some conversion objects
86
+ this.aqW2HK = {
87
+ 0: 5, // poor -> poor
88
+ 1: 3, // moderate -> fair
89
+ 2: 1, // good -> excellent
90
+ }
91
+ this.aqLabels = {
92
+ 5: platformLang.labelPoor,
93
+ 3: platformLang.labelFair,
94
+ 1: platformLang.labelExc,
95
+ }
96
+ this.modeLabels = {
97
+ 0: platformLang.labelOff,
98
+ 1: platformLang.labelLow,
99
+ 2: platformLang.labelMed,
100
+ 3: platformLang.labelHigh,
101
+ 4: platformLang.labelAuto,
102
+ }
103
+
104
+ // Output the customised options to the log
105
+ const opts = JSON.stringify({
106
+ })
107
+ platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
108
+
109
+ // Request a device update immediately
110
+ this.requestDeviceUpdate()
111
+
112
+ // Start a polling interval if the user has disabled upnp
113
+ if (this.accessory.context.connection === 'http') {
114
+ this.pollingInterval = setInterval(
115
+ () => this.requestDeviceUpdate(),
116
+ platform.config.pollingInterval * 1000,
117
+ )
118
+ }
119
+ }
120
+
121
+ receiveDeviceUpdate(attribute) {
122
+ // Log the receiving update if debug is enabled
123
+ this.accessory.logDebug(`${platformLang.recUpd} [${attribute.name}: ${JSON.stringify(attribute.value)}]`)
124
+
125
+ // Check which attribute we are getting
126
+ switch (attribute.name) {
127
+ case 'AirQuality':
128
+ this.externalAirQualityUpdate(attribute.value)
129
+ break
130
+ case 'ExpiredFilterTime':
131
+ this.externalFilterChangeUpdate(attribute.value !== 0 ? 1 : 0)
132
+ break
133
+ case 'FilterLife':
134
+ this.externalFilterLifeUpdate(Math.round((attribute.value / 60480) * 100))
135
+ break
136
+ case 'Ionizer':
137
+ this.externalIonizerUpdate(attribute.value)
138
+ break
139
+ case 'Mode':
140
+ this.externalModeUpdate(attribute.value)
141
+ break
142
+ default:
143
+ }
144
+ }
145
+
146
+ async requestDeviceUpdate() {
147
+ try {
148
+ // Request the update
149
+ const data = await this.platform.httpClient.sendDeviceUpdate(
150
+ this.accessory,
151
+ 'urn:Belkin:service:deviceevent:1',
152
+ 'GetAttributes',
153
+ )
154
+
155
+ // Parse the response
156
+ const decoded = decodeXML(data.attributeList)
157
+ const xml = `<attributeList>${decoded}</attributeList>`
158
+ const result = await parseStringPromise(xml, { explicitArray: false })
159
+ Object.keys(result.attributeList.attribute).forEach((key) => {
160
+ // Only send the required attributes to the receiveDeviceUpdate function
161
+ switch (result.attributeList.attribute[key].name) {
162
+ case 'AirQuality':
163
+ case 'ExpiredFilterTime':
164
+ case 'FilterLife':
165
+ case 'Ionizer':
166
+ case 'Mode':
167
+ this.receiveDeviceUpdate({
168
+ name: result.attributeList.attribute[key].name,
169
+ value: Number.parseInt(result.attributeList.attribute[key].value, 10),
170
+ })
171
+ break
172
+ default:
173
+ }
174
+ })
175
+ } catch (err) {
176
+ const eText = parseError(err, [
177
+ platformLang.timeout,
178
+ platformLang.timeoutUnreach,
179
+ platformLang.noService,
180
+ ])
181
+ this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`)
182
+ }
183
+ }
184
+
185
+ async sendDeviceUpdate(attributes) {
186
+ // Log the sending update if debug is enabled
187
+ this.accessory.log(`${platformLang.senUpd} ${JSON.stringify(attributes)}`)
188
+
189
+ // Generate the XML to send
190
+ const builder = new Builder({
191
+ rootName: 'attribute',
192
+ headless: true,
193
+ renderOpts: { pretty: false },
194
+ })
195
+ const xmlAttributes = Object.keys(attributes)
196
+ .map(attributeKey => builder.buildObject({
197
+ name: attributeKey,
198
+ value: attributes[attributeKey],
199
+ }))
200
+ .join('')
201
+
202
+ // Send the update
203
+ await this.platform.httpClient.sendDeviceUpdate(
204
+ this.accessory,
205
+ 'urn:Belkin:service:deviceevent:1',
206
+ 'SetAttributes',
207
+ {
208
+ attributeList: { '#text': xmlAttributes },
209
+ },
210
+ )
211
+ }
212
+
213
+ async internalStateUpdate(value) {
214
+ const prevState = this.service.getCharacteristic(this.hapChar.Active).value
215
+ try {
216
+ // Don't continue if the state is the same as before
217
+ if (value === prevState) {
218
+ return
219
+ }
220
+
221
+ // We also want to update the mode (by rotation speed)
222
+ let newSpeed = 0
223
+ if (value !== 0) {
224
+ // If turning on then we want to show the last used mode (by rotation speed)
225
+ switch (this.accessory.context.cacheLastOnMode) {
226
+ case 2:
227
+ newSpeed = 50
228
+ break
229
+ case 3:
230
+ newSpeed = 75
231
+ break
232
+ case 4:
233
+ newSpeed = 100
234
+ break
235
+ default:
236
+ newSpeed = 25
237
+ }
238
+ }
239
+
240
+ // Update the rotation speed, use setCharacteristic so the set handler is run to send updates
241
+ this.service.setCharacteristic(this.hapChar.RotationSpeed, newSpeed)
242
+
243
+ // Update the characteristic if we are now ON ie purifying air
244
+ this.service.updateCharacteristic(
245
+ this.hapChar.CurrentAirPurifierState,
246
+ newSpeed === 0 ? 0 : 2,
247
+ )
248
+
249
+ // Update the ionizer characteristic if the purifier is on and the ionizer was on before
250
+ this.ioService.updateCharacteristic(
251
+ this.hapChar.On,
252
+ value === 1 && this.accessory.context.cacheIonizerOn === 1,
253
+ )
254
+ } catch (err) {
255
+ const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach])
256
+ this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`)
257
+
258
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
259
+ setTimeout(() => {
260
+ this.service.updateCharacteristic(this.hapChar.Active, prevState)
261
+ }, 2000)
262
+ throw new this.hapErr(-70402)
263
+ }
264
+ }
265
+
266
+ async internalModeUpdate(value) {
267
+ const prevSpeed = this.service.getCharacteristic(this.hapChar.RotationSpeed).value
268
+ try {
269
+ // Avoid multiple updates in quick succession
270
+ const updateKey = generateRandomString(5)
271
+ this.updateKey = updateKey
272
+ await sleep(500)
273
+ if (updateKey !== this.updateKey) {
274
+ return
275
+ }
276
+
277
+ // Don't continue if the speed is the same as before
278
+ if (value === prevSpeed) {
279
+ return
280
+ }
281
+
282
+ // Generate newValue for the needed mode depending on the new rotation speed value
283
+ let newValue = 0
284
+ if (value > 10 && value <= 35) {
285
+ newValue = 1
286
+ } else if (value > 35 && value <= 60) {
287
+ newValue = 2
288
+ } else if (value > 60 && value <= 85) {
289
+ newValue = 3
290
+ } else if (value > 85) {
291
+ newValue = 4
292
+ }
293
+
294
+ // Send the update
295
+ await this.sendDeviceUpdate({
296
+ Mode: newValue.toString(),
297
+ })
298
+
299
+ // Update the cache last used mode if not turning off
300
+ if (newValue !== 0) {
301
+ this.accessory.context.cacheLastOnMode = newValue
302
+ }
303
+
304
+ // Log the new mode if appropriate
305
+ this.accessory.log(`${platformLang.curMode} [${this.modeLabels[newValue]}]`)
306
+ } catch (err) {
307
+ const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach])
308
+ this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`)
309
+
310
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
311
+ setTimeout(() => {
312
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, prevSpeed)
313
+ }, 2000)
314
+ throw new this.hapErr(-70402)
315
+ }
316
+ }
317
+
318
+ async internalIonizerUpdate(value) {
319
+ const prevState = this.ioService.getCharacteristic(this.hapChar.On).value
320
+ try {
321
+ // If turning on, but the purifier device is off, then turn the ionizer back off
322
+ if (value && this.service.getCharacteristic(this.hapChar.Active).value === 0) {
323
+ await sleep(1000)
324
+ this.ioService.updateCharacteristic(this.hapChar.On, false)
325
+ return
326
+ }
327
+
328
+ // Send the update
329
+ await this.sendDeviceUpdate({
330
+ Ionizer: value ? 1 : 0,
331
+ })
332
+
333
+ // Update the cache state of the ionizer
334
+ this.accessory.context.cacheIonizerOn = value ? 1 : 0
335
+
336
+ // Log the update if appropriate
337
+ this.accessory.log(`${platformLang.curIon} [${value ? 'on' : 'off'}}]`)
338
+ } catch (err) {
339
+ const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach])
340
+ this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`)
341
+
342
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
343
+ setTimeout(() => {
344
+ this.ioService.updateCharacteristic(this.hapChar.On, prevState)
345
+ }, 2000)
346
+ throw new this.hapErr(-70402)
347
+ }
348
+ }
349
+
350
+ externalModeUpdate(value) {
351
+ try {
352
+ // We want to find a rotation speed based on the given mode
353
+ let rotSpeed = 0
354
+ switch (value) {
355
+ case 1: {
356
+ rotSpeed = 25
357
+ break
358
+ }
359
+ case 2: {
360
+ rotSpeed = 50
361
+ break
362
+ }
363
+ case 3: {
364
+ rotSpeed = 75
365
+ break
366
+ }
367
+ case 4: {
368
+ rotSpeed = 100
369
+ break
370
+ }
371
+ default:
372
+ return
373
+ }
374
+
375
+ // Update the HomeKit characteristics
376
+ this.service.updateCharacteristic(this.hapChar.Active, value !== 0 ? 1 : 0)
377
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, rotSpeed)
378
+
379
+ // Turn the ionizer on or off based on whether the purifier is on or off
380
+ if (value === 0) {
381
+ this.ioService.updateCharacteristic(this.hapChar.On, false)
382
+ } else {
383
+ this.ioService.updateCharacteristic(
384
+ this.hapChar.On,
385
+ this.accessory.context.cacheIonizerOn === 1,
386
+ )
387
+ this.accessory.context.cacheLastOnMode = value
388
+ }
389
+
390
+ // Log the change if appropriate
391
+ this.accessory.log(`${platformLang.curMode} [${this.modeLabels[value]}]`)
392
+ } catch (err) {
393
+ this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`)
394
+ }
395
+ }
396
+
397
+ externalAirQualityUpdate(value) {
398
+ try {
399
+ const newValue = this.aqW2HK[value]
400
+ // Don't continue if the value is the same as before
401
+ if (this.airService.getCharacteristic(this.hapChar.AirQuality).value === newValue) {
402
+ return
403
+ }
404
+
405
+ // Update the HomeKit characteristics
406
+ this.airService.updateCharacteristic(this.hapChar.AirQuality, newValue)
407
+
408
+ // Log the change if appropriate
409
+ this.accessory.log(`${platformLang.curAir} [${this.aqLabels[newValue]}]`)
410
+ } catch (err) {
411
+ this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`)
412
+ }
413
+ }
414
+
415
+ externalIonizerUpdate(value) {
416
+ try {
417
+ // Don't continue if the value is the same as before
418
+ const state = this.ioService.getCharacteristic(this.hapChar.On).value ? 1 : 0
419
+ if (state === value) {
420
+ return
421
+ }
422
+
423
+ // Update the HomeKit characteristics
424
+ this.ioService.updateCharacteristic(this.hapChar.On, value === 1)
425
+
426
+ // Update the cache value and log the change if appropriate
427
+ this.accessory.context.cacheIonizerOn = value
428
+ this.accessory.log(`${platformLang.curIon} [${value === 1 ? 'on' : 'off'}]`)
429
+ } catch (err) {
430
+ this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`)
431
+ }
432
+ }
433
+
434
+ externalFilterChangeUpdate(value) {
435
+ try {
436
+ // Don't continue if the value is the same as before
437
+ if (value === this.cacheFilterX) {
438
+ return
439
+ }
440
+
441
+ // Update the HomeKit characteristics
442
+ this.service.updateCharacteristic(this.hapChar.FilterChangeIndication, value)
443
+
444
+ // Update the cache value and log the change if appropriate
445
+ this.cacheFilterX = value
446
+ } catch (err) {
447
+ this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`)
448
+ }
449
+ }
450
+
451
+ externalFilterLifeUpdate(value) {
452
+ try {
453
+ // Don't continue if the value is the same as before
454
+ if (value === this.cacheFilter) {
455
+ return
456
+ }
457
+
458
+ // Update the HomeKit characteristics
459
+ this.service.updateCharacteristic(this.hapChar.FilterLifeLevel, value)
460
+
461
+ // Update the cache value and log the change if appropriate
462
+ this.cacheFilter = value
463
+ this.accessory.log(`${platformLang.curFilter} [${value}%]`)
464
+ } catch (err) {
465
+ this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`)
466
+ }
467
+ }
468
+ }