@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,467 @@
1
+ import { parseStringPromise } from 'xml2js'
2
+
3
+ import {
4
+ hs2rgb,
5
+ rgb2hs,
6
+ rgb2xy,
7
+ xy2rgb,
8
+ } from '../utils/colour.js'
9
+ import platformConsts from '../utils/constants.js'
10
+ import { generateRandomString, parseError, sleep } from '../utils/functions.js'
11
+ import platformLang from '../utils/lang-en.js'
12
+
13
+ export default class {
14
+ constructor(platform, priAcc, accessory) {
15
+ // Set up variables from the platform
16
+ this.hapChar = platform.api.hap.Characteristic
17
+ this.hapErr = platform.api.hap.HapStatusError
18
+ this.hapServ = platform.api.hap.Service
19
+ this.platform = platform
20
+
21
+ // Set up variables from the accessory
22
+ this.accessory = accessory
23
+ this.priAcc = priAcc
24
+
25
+ // Set up variables from the device
26
+ this.deviceID = accessory.context.deviceId
27
+
28
+ // Set up custom variables for this device type
29
+ const deviceConf = platform.deviceConf[this.deviceID] || {}
30
+ this.brightStep = deviceConf.brightnessStep
31
+ ? Math.min(deviceConf.brightnessStep, 100)
32
+ : platformConsts.defaultValues.brightnessStep
33
+ this.alShift = deviceConf.adaptiveLightingShift || platformConsts.defaultValues.adaptiveLightingShift
34
+ this.transitionTime = deviceConf.transitionTime || platformConsts.defaultValues.transitionTime
35
+
36
+ // Objects containing mapping info for the device capabilities
37
+ this.linkCodes = {
38
+ switch: '10006',
39
+ brightness: '10008',
40
+ color: '10300',
41
+ temperature: '30301',
42
+ }
43
+ this.linkCodesRev = {
44
+ 10600: 'switch',
45
+ 10008: 'brightness',
46
+ 10300: 'color',
47
+ 30301: 'temperature',
48
+ }
49
+
50
+ // Quick check variables for later use
51
+ this.hasBrightSupport = accessory.context.capabilities[this.linkCodes.brightness]
52
+ this.hasColourSupport = accessory.context.capabilities[this.linkCodes.color]
53
+ && deviceConf?.enableColourControl
54
+ this.hasCTempSupport = accessory.context.capabilities[this.linkCodes.temperature]
55
+
56
+ // Add the lightbulb service if it doesn't already exist
57
+ this.service = this.accessory.getService(this.hapServ.Lightbulb)
58
+ || this.accessory.addService(this.hapServ.Lightbulb)
59
+
60
+ // If adaptive lighting has just been disabled then remove and re-add service to hide AL icon
61
+ if (this.alShift === -1 && this.accessory.context.adaptiveLighting) {
62
+ this.accessory.removeService(this.service)
63
+ this.service = this.accessory.addService(this.hapServ.Lightbulb)
64
+ this.accessory.context.adaptiveLighting = false
65
+ }
66
+
67
+ // Add the set handler to the lightbulb on/off characteristic
68
+ this.service
69
+ .getCharacteristic(this.hapChar.On)
70
+ .removeOnSet()
71
+ .onSet(async value => this.internalStateUpdate(value))
72
+
73
+ // Add the set handler to the brightness characteristic if supported
74
+ if (this.hasBrightSupport) {
75
+ this.service
76
+ .getCharacteristic(this.hapChar.Brightness)
77
+ .setProps({ minStep: this.brightStep })
78
+ .onSet(async (value) => {
79
+ await this.internalBrightnessUpdate(value)
80
+ })
81
+ }
82
+
83
+ // Add the set handler to the colour temperature characteristic if supported
84
+ if (this.hasColourSupport) {
85
+ this.service.getCharacteristic(this.hapChar.Hue).onSet(async (value) => {
86
+ await this.internalColourUpdate(value)
87
+ })
88
+ this.cacheHue = this.service.getCharacteristic(this.hapChar.Hue).value
89
+ this.cacheSat = this.service.getCharacteristic(this.hapChar.Saturation).value
90
+ } else {
91
+ if (this.service.testCharacteristic(this.hapChar.Hue)) {
92
+ this.service.removeCharacteristic(this.service.getCharacteristic(this.hapChar.Hue))
93
+ }
94
+ if (this.service.testCharacteristic(this.hapChar.Saturation)) {
95
+ this.service.removeCharacteristic(this.service.getCharacteristic(this.hapChar.Saturation))
96
+ }
97
+ }
98
+
99
+ // Add the set handler to the colour temperature characteristic if supported
100
+ if (this.hasCTempSupport) {
101
+ this.service.getCharacteristic(this.hapChar.ColorTemperature).onSet(async (value) => {
102
+ await this.internalCTUpdate(value)
103
+ })
104
+ this.cacheMired = this.service.getCharacteristic(this.hapChar.ColorTemperature).value
105
+
106
+ // Add support for adaptive lighting if not disabled by user
107
+ if (this.alShift !== -1) {
108
+ this.alController = new platform.api.hap.AdaptiveLightingController(this.service, {
109
+ customTemperatureAdjustment: this.alShift,
110
+ })
111
+ this.accessory.configureController(this.alController)
112
+ }
113
+ }
114
+
115
+ // Output the customised options to the log
116
+ const opts = JSON.stringify({
117
+ adaptiveLightingShift: this.alShift,
118
+ brightnessStep: this.brightStep,
119
+ transitionTime: this.transitionTime,
120
+ })
121
+ platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
122
+
123
+ // Request a device update immediately
124
+ this.requestDeviceUpdate()
125
+ }
126
+
127
+ receiveDeviceUpdate(attribute) {
128
+ // Log the receiving update if debug is enabled
129
+ this.accessory.logDebug(`${platformLang.recUpd} [${this.linkCodesRev[attribute.name]}: ${attribute.value}]`)
130
+
131
+ // Check which attribute we are getting
132
+ switch (attribute.name) {
133
+ case this.linkCodes.switch:
134
+ // Need a HomeKit true/false value for the state update
135
+ this.externalStateUpdate(Number.parseInt(attribute.value, 10) !== 0)
136
+ break
137
+ case this.linkCodes.brightness:
138
+ // Need a HomeKit int value for the brightness update
139
+ this.externalBrightnessUpdate(Math.round(attribute.value.split(':').shift() / 2.55))
140
+ break
141
+ case this.linkCodes.color: {
142
+ if (this.hasColourSupport) {
143
+ // Need a HomeKit int values for the colour update
144
+ const xy = attribute.value.split(':')
145
+ this.externalColourUpdate(xy[0], xy[1])
146
+ }
147
+ break
148
+ }
149
+ case this.linkCodes.temperature:
150
+ // Need a HomeKit int value for the colour temperature update
151
+ this.externalCTUpdate(Math.round(attribute.value.split(':').shift()))
152
+ break
153
+ default:
154
+ }
155
+ }
156
+
157
+ async sendDeviceUpdate(capability, value) {
158
+ // Log the sending update if debug is enabled
159
+ this.accessory.logDebug(`${platformLang.senUpd} [${capability}: ${value}]`)
160
+
161
+ // Send the update
162
+ await this.priAcc.control.sendDeviceUpdate(
163
+ this.accessory.context.serialNumber,
164
+ capability,
165
+ value,
166
+ )
167
+ }
168
+
169
+ async requestDeviceUpdate() {
170
+ try {
171
+ // Request the update via the main (hidden) accessory
172
+ const data = await this.priAcc.control.requestDeviceUpdate(
173
+ this.accessory.context.serialNumber,
174
+ )
175
+
176
+ // Parse the response
177
+ const res = await parseStringPromise(data.DeviceStatusList, { explicitArray: false })
178
+ const deviceStatus = res.DeviceStatusList.DeviceStatus
179
+ const values = deviceStatus.CapabilityValue.split(',')
180
+ const caps = {}
181
+ deviceStatus.CapabilityID.split(',').forEach((val, index) => {
182
+ caps[val] = values[index]
183
+ })
184
+
185
+ // If no capability values received then device must be offline
186
+ if (!caps[this.linkCodes.switch] || !caps[this.linkCodes.switch].length) {
187
+ this.accessory.logWarn(platformLang.devOffline)
188
+ return
189
+ }
190
+
191
+ // Need a HomeKit true/false value for the state update
192
+ if (caps[this.linkCodes.switch]) {
193
+ this.externalStateUpdate(Number.parseInt(caps[this.linkCodes.switch], 10) !== 0)
194
+ }
195
+
196
+ // Need a HomeKit int value for the brightness update
197
+ if (caps[this.linkCodes.brightness] && this.hasBrightSupport) {
198
+ this.externalBrightnessUpdate(
199
+ Math.round(caps[this.linkCodes.brightness].split(':').shift() / 2.55),
200
+ )
201
+ }
202
+
203
+ // Need a HomeKit int value for the colour update
204
+ if (caps[this.linkCodes.color] && this.hasColourSupport) {
205
+ const xy = caps[this.linkCodes.color].split(':')
206
+ this.externalColourUpdate(xy[0], xy[1])
207
+ }
208
+
209
+ // Need a HomeKit int value for the colour temperature update
210
+ if (caps[this.linkCodes.temperature] && this.hasCTempSupport) {
211
+ this.externalCTUpdate(Math.round(caps[this.linkCodes.temperature].split(':').shift()))
212
+ }
213
+ } catch (err) {
214
+ const eText = parseError(err, [
215
+ platformLang.timeout,
216
+ platformLang.timeoutUnreach,
217
+ platformLang.noService,
218
+ ])
219
+ this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`)
220
+ }
221
+ }
222
+
223
+ async internalStateUpdate(value) {
224
+ try {
225
+ // Wait a longer time than the brightness so in scenes brightness is sent first
226
+ await sleep(500)
227
+
228
+ // Send the update
229
+ await this.sendDeviceUpdate(this.linkCodes.switch, value ? 1 : 0)
230
+
231
+ // Update the cache and log if appropriate
232
+ this.cacheState = value
233
+ this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`)
234
+ } catch (err) {
235
+ const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach])
236
+ this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`)
237
+
238
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
239
+ setTimeout(() => {
240
+ this.service.updateCharacteristic(this.hapChar.On, this.cacheState)
241
+ }, 2000)
242
+ throw new this.hapErr(-70402)
243
+ }
244
+ }
245
+
246
+ async internalBrightnessUpdate(value) {
247
+ try {
248
+ // Avoid multiple updates in quick succession
249
+ const updateKey = generateRandomString(5)
250
+ this.updateKeyBR = updateKey
251
+ await sleep(300)
252
+ if (updateKey !== this.updateKeyBR) {
253
+ return
254
+ }
255
+
256
+ // Don't continue if this value is same as before
257
+ if (this.cacheBright === value) {
258
+ return
259
+ }
260
+
261
+ // Send the update - value = brightness:transition_time
262
+ await this.sendDeviceUpdate(
263
+ this.linkCodes.brightness,
264
+ `${value * 2.55}:${this.transitionTime}`,
265
+ )
266
+
267
+ // Update the cache and log if appropriate
268
+ this.cacheBright = value
269
+ this.accessory.log(`${platformLang.curBright} [${this.cacheBright}%]`)
270
+ } catch (err) {
271
+ const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach])
272
+ this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`)
273
+
274
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
275
+ setTimeout(() => {
276
+ this.service.updateCharacteristic(this.hapChar.Brightness, this.cacheBright)
277
+ }, 2000)
278
+ throw new this.hapErr(-70402)
279
+ }
280
+ }
281
+
282
+ async internalColourUpdate(value) {
283
+ try {
284
+ // Avoid multiple updates in quick succession
285
+ const updateKey = generateRandomString(5)
286
+ this.updateKeyHue = updateKey
287
+ await sleep(400)
288
+ if (updateKey !== this.updateKeyHue) {
289
+ return
290
+ }
291
+
292
+ // Don't continue if this value is same as before
293
+ if (this.cacheHue === value) {
294
+ return
295
+ }
296
+
297
+ // First convert to RGB
298
+ const currentSat = this.service.getCharacteristic(this.hapChar.Saturation).value
299
+ const [r, g, b] = hs2rgb(value, currentSat)
300
+
301
+ // Then convert the RGB to the values needed for Wemo
302
+ const [x, y] = rgb2xy(r, g, b)
303
+ const X = Math.round(x * 65535)
304
+ const Y = Math.round(y * 65535)
305
+
306
+ // Send the update - value = ct:transition_time
307
+ await this.sendDeviceUpdate(this.linkCodes.color, `${X}:${Y}:${this.transitionTime}`)
308
+
309
+ // Update the cache and log if appropriate
310
+ this.cacheHue = value
311
+ this.cacheSat = currentSat
312
+ this.cacheMired = 0
313
+ this.accessory.log(`${platformLang.curColour} [X:${X} Y:${Y}]`)
314
+ } catch (err) {
315
+ const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach])
316
+ this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`)
317
+
318
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
319
+ setTimeout(() => {
320
+ this.service.updateCharacteristic(this.hapChar.Hue, this.cacheHue)
321
+ }, 2000)
322
+ throw new this.hapErr(-70402)
323
+ }
324
+ }
325
+
326
+ async internalCTUpdate(value) {
327
+ try {
328
+ // Avoid multiple updates in quick succession
329
+ const updateKey = generateRandomString(5)
330
+ this.updateKeyCT = updateKey
331
+ await sleep(400)
332
+ if (updateKey !== this.updateKeyCT) {
333
+ return
334
+ }
335
+
336
+ // Value needs to be between 170 and 370
337
+ value = Math.min(Math.max(value, 170), 370)
338
+
339
+ // Don't continue if this value is same as before
340
+ if (this.cacheMired === value) {
341
+ return
342
+ }
343
+
344
+ // Send the update - value = ct:transition_time
345
+ await this.sendDeviceUpdate(this.linkCodes.temperature, `${value}:${this.transitionTime}`)
346
+
347
+ // Update the cache and log if appropriate
348
+ this.cacheMired = value
349
+ this.cacheHue = 0
350
+ this.cacheSat = 0
351
+ // Convert mired value to kelvin for logging
352
+ const mToK = Math.round(1000000 / value)
353
+ if (this.alController?.isAdaptiveLightingActive()) {
354
+ this.accessory.log(`${platformLang.curCCT} [${mToK}K / ${value}M] ${platformLang.viaAL}`)
355
+ } else {
356
+ this.accessory.log(`${platformLang.curCCT} [${mToK}K / ${value}M]`)
357
+ }
358
+ } catch (err) {
359
+ const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach])
360
+ this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`)
361
+
362
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
363
+ setTimeout(() => {
364
+ this.service.updateCharacteristic(this.hapChar.ColorTemperature, this.cacheMired)
365
+ }, 2000)
366
+ throw new this.hapErr(-70402)
367
+ }
368
+ }
369
+
370
+ externalStateUpdate(value) {
371
+ try {
372
+ // Don't continue if the state is the same as before
373
+ if (value === this.cacheState) {
374
+ return
375
+ }
376
+
377
+ // Update the state HomeKit characteristic
378
+ this.service.updateCharacteristic(this.hapChar.On, value)
379
+
380
+ // Update the cache and log if appropriate
381
+ this.cacheState = value
382
+ this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`)
383
+ } catch (err) {
384
+ this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`)
385
+ }
386
+ }
387
+
388
+ externalBrightnessUpdate(value) {
389
+ try {
390
+ // Don't continue if the brightness is the same as before
391
+ if (value === this.cacheBright) {
392
+ return
393
+ }
394
+
395
+ // Update the brightness HomeKit characteristic
396
+ this.service.updateCharacteristic(this.hapChar.Brightness, value)
397
+
398
+ // Update the cache and log if appropriate
399
+ this.cacheBright = value
400
+ this.accessory.log(`${platformLang.curBright} [${this.cacheBright}%]`)
401
+ } catch (err) {
402
+ this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`)
403
+ }
404
+ }
405
+
406
+ externalColourUpdate(valueX, valueY) {
407
+ try {
408
+ // Convert the given values to RGB and hue/saturation
409
+ const [r, g, b] = xy2rgb(valueX / 65535, valueY / 65535)
410
+ const [h, s] = rgb2hs(r, g, b)
411
+
412
+ // Don't continue if the hue and saturation are the same as before
413
+ if (this.cacheHue !== h || this.cacheSat !== s) {
414
+ // Update the HomeKit characteristics
415
+ this.service.updateCharacteristic(this.hapChar.ColorTemperature, 140)
416
+ this.service.updateCharacteristic(this.hapChar.Hue, h)
417
+ this.service.updateCharacteristic(this.hapChar.Saturation, s)
418
+
419
+ // Update the cache values
420
+ this.cacheMired = 0
421
+ this.cacheHue = h
422
+ this.cacheSat = s
423
+
424
+ // Log the change if appropriate
425
+ this.accessory.log(`${platformLang.curColour} [X:${valueX} Y:${valueY}]`)
426
+
427
+ // Colour chosen externally so disable adaptive lighting
428
+ if (this.alController?.isAdaptiveLightingActive()) {
429
+ this.alController.disableAdaptiveLighting()
430
+ this.accessory.log(platformLang.alDisabled)
431
+ }
432
+ }
433
+ } catch (err) {
434
+ this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`)
435
+ }
436
+ }
437
+
438
+ externalCTUpdate(value) {
439
+ try {
440
+ // Don't continue if the mired value is the same as before
441
+ if (value === this.cacheMired) {
442
+ return
443
+ }
444
+
445
+ // Update the mired HomeKit characteristic
446
+ this.service.updateCharacteristic(this.hapChar.ColorTemperature, value)
447
+
448
+ // Log the change if appropriate
449
+ const mToK = Math.round(1000000 / value)
450
+ this.accessory.log(`${platformLang.curCCT} [${mToK}K / ${value}M]`)
451
+
452
+ // If the difference is significant (>20) then disable adaptive lighting
453
+ if (!Number.isNaN(this.cacheMired)) {
454
+ const diff = Math.abs(value - this.cacheMired) > 20
455
+ if (this.alController?.isAdaptiveLightingActive() && diff) {
456
+ this.alController.disableAdaptiveLighting()
457
+ this.accessory.log(platformLang.alDisabled)
458
+ }
459
+ }
460
+
461
+ // Update the cache value after the adaptive lighting check
462
+ this.cacheMired = value
463
+ } catch (err) {
464
+ this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`)
465
+ }
466
+ }
467
+ }
@@ -0,0 +1,63 @@
1
+ import { create as xmlCreate } from 'xmlbuilder'
2
+
3
+ export default class {
4
+ constructor(platform, accessory, devicesInHB) {
5
+ // Set up variables from the platform
6
+ this.devicesInHB = devicesInHB
7
+ this.hapChar = platform.api.hap.Characteristic
8
+ this.hapErr = platform.api.hap.HapStatusError
9
+ this.hapServ = platform.api.hap.Service
10
+ this.platform = platform
11
+
12
+ // Set up variables from the accessory
13
+ this.accessory = accessory
14
+ }
15
+
16
+ receiveDeviceUpdate(deviceId, attribute) {
17
+ // Find the accessory to which this relates
18
+ this.devicesInHB.forEach((accessory) => {
19
+ if (
20
+ accessory.context.serialNumber === deviceId
21
+ && accessory.control?.receiveDeviceUpdate
22
+ ) {
23
+ accessory.control.receiveDeviceUpdate(attribute)
24
+ }
25
+ })
26
+ }
27
+
28
+ async sendDeviceUpdate(deviceId, capability, value) {
29
+ // Generate the XML to send
30
+ const deviceStatusList = xmlCreate('DeviceStatus', {
31
+ version: '1.0',
32
+ encoding: 'utf-8',
33
+ })
34
+ .ele({
35
+ IsGroupAction: deviceId.length === 10 ? 'YES' : 'NO',
36
+ DeviceID: deviceId,
37
+ CapabilityID: capability,
38
+ CapabilityValue: value,
39
+ })
40
+ .end()
41
+
42
+ // Send the update
43
+ return this.platform.httpClient.sendDeviceUpdate(
44
+ this.accessory,
45
+ 'urn:Belkin:service:bridge:1',
46
+ 'SetDeviceStatus',
47
+ {
48
+ DeviceStatusList: { '#text': deviceStatusList },
49
+ },
50
+ )
51
+ }
52
+
53
+ async requestDeviceUpdate(deviceId) {
54
+ return this.platform.httpClient.sendDeviceUpdate(
55
+ this.accessory,
56
+ 'urn:Belkin:service:bridge:1',
57
+ 'GetDeviceStatus',
58
+ {
59
+ DeviceIDs: deviceId,
60
+ },
61
+ )
62
+ }
63
+ }