@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,416 @@
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 heater service if it doesn't already exist
23
+ this.service = this.accessory.getService(this.hapServ.HeaterCooler)
24
+ || this.accessory.addService(this.hapServ.HeaterCooler)
25
+
26
+ // Add the set handler to the heater active characteristic
27
+ this.service
28
+ .getCharacteristic(this.hapChar.Active)
29
+ .removeOnSet()
30
+ .onSet(async value => this.internalStateUpdate(value))
31
+
32
+ // Add options to the heater target state characteristic
33
+ this.service.getCharacteristic(this.hapChar.TargetHeaterCoolerState).setProps({
34
+ minValue: 0,
35
+ maxValue: 0,
36
+ validValues: [0],
37
+ })
38
+
39
+ // Add the set handler and a range to the heater target temperature characteristic
40
+ this.service
41
+ .getCharacteristic(this.hapChar.HeatingThresholdTemperature)
42
+ .setProps({
43
+ minStep: 1,
44
+ minValue: 16,
45
+ maxValue: 29,
46
+ })
47
+ .onSet(async (value) => {
48
+ await this.internalTargetTempUpdate(value)
49
+ })
50
+
51
+ // Add the set handler to the heater rotation speed characteristic
52
+ this.service
53
+ .getCharacteristic(this.hapChar.RotationSpeed)
54
+ .setProps({ minStep: 33 })
55
+ .onSet(async (value) => {
56
+ await this.internalModeUpdate(value)
57
+ })
58
+
59
+ // Add a last mode cache value if not already set
60
+ const cacheMode = this.accessory.context.cacheLastOnMode
61
+ if (!cacheMode || [0, 1].includes(cacheMode)) {
62
+ this.accessory.context.cacheLastOnMode = 4
63
+ }
64
+
65
+ // Add a last temperature cache value if not already set
66
+ if (!this.accessory.context.cacheLastOnTemp) {
67
+ this.accessory.context.cacheLastOnTemp = 16
68
+ }
69
+
70
+ // Some conversion objects
71
+ this.modeLabels = {
72
+ 0: platformLang.labelOff,
73
+ 1: platformLang.labelFP,
74
+ 2: platformLang.labelHigh,
75
+ 3: platformLang.labelLow,
76
+ 4: platformLang.labelEco,
77
+ }
78
+ this.cToF = {
79
+ 16: 61,
80
+ 17: 63,
81
+ 18: 64,
82
+ 19: 66,
83
+ 20: 68,
84
+ 21: 70,
85
+ 22: 72,
86
+ 23: 73,
87
+ 24: 75,
88
+ 25: 77,
89
+ 26: 79,
90
+ 27: 81,
91
+ 28: 83,
92
+ 29: 84,
93
+ }
94
+
95
+ // Output the customised options to the log
96
+ const opts = JSON.stringify({
97
+ })
98
+ platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
99
+
100
+ // Request a device update immediately
101
+ this.requestDeviceUpdate()
102
+
103
+ // Start a polling interval if the user has disabled upnp
104
+ if (this.accessory.context.connection === 'http') {
105
+ this.pollingInterval = setInterval(
106
+ () => this.requestDeviceUpdate(),
107
+ platform.config.pollingInterval * 1000,
108
+ )
109
+ }
110
+ }
111
+
112
+ receiveDeviceUpdate(attribute) {
113
+ // Log the receiving update if debug is enabled
114
+ this.accessory.logDebug(`${platformLang.recUpd} [${attribute.name}: ${JSON.stringify(attribute.value)}]`)
115
+
116
+ // Check which attribute we are getting
117
+ switch (attribute.name) {
118
+ case 'Mode':
119
+ this.externalModeUpdate(attribute.value)
120
+ break
121
+ case 'Temperature':
122
+ this.externalCurrentTempUpdate(attribute.value)
123
+ break
124
+ case 'SetTemperature':
125
+ this.externalTargetTempUpdate(attribute.value)
126
+ break
127
+ default:
128
+ }
129
+ }
130
+
131
+ async requestDeviceUpdate() {
132
+ try {
133
+ // Request the update
134
+ const data = await this.platform.httpClient.sendDeviceUpdate(
135
+ this.accessory,
136
+ 'urn:Belkin:service:deviceevent:1',
137
+ 'GetAttributes',
138
+ )
139
+
140
+ // Parse the response
141
+ const decoded = decodeXML(data.attributeList)
142
+ const xml = `<attributeList>${decoded}</attributeList>`
143
+ const result = await parseStringPromise(xml, { explicitArray: false })
144
+ Object.keys(result.attributeList.attribute).forEach((key) => {
145
+ // Only send the required attributes to the receiveDeviceUpdate function
146
+ switch (result.attributeList.attribute[key].name) {
147
+ case 'Mode':
148
+ case 'Temperature':
149
+ case 'SetTemperature':
150
+ this.receiveDeviceUpdate({
151
+ name: result.attributeList.attribute[key].name,
152
+ value: Number.parseInt(result.attributeList.attribute[key].value, 10),
153
+ })
154
+ break
155
+ default:
156
+ }
157
+ })
158
+ } catch (err) {
159
+ const eText = parseError(err, [
160
+ platformLang.timeout,
161
+ platformLang.timeoutUnreach,
162
+ platformLang.noService,
163
+ ])
164
+ this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`)
165
+ }
166
+ }
167
+
168
+ async sendDeviceUpdate(attributes) {
169
+ // Log the sending update if debug is enabled
170
+ this.accessory.log(`${platformLang.senUpd} ${JSON.stringify(attributes)}`)
171
+
172
+ // Generate the XML to send
173
+ const builder = new Builder({
174
+ rootName: 'attribute',
175
+ headless: true,
176
+ renderOpts: { pretty: false },
177
+ })
178
+ const xmlAttributes = Object.keys(attributes)
179
+ .map(attributeKey => builder.buildObject({
180
+ name: attributeKey,
181
+ value: attributes[attributeKey],
182
+ }))
183
+ .join('')
184
+
185
+ // Send the update
186
+ await this.platform.httpClient.sendDeviceUpdate(
187
+ this.accessory,
188
+ 'urn:Belkin:service:deviceevent:1',
189
+ 'SetAttributes',
190
+ {
191
+ attributeList: { '#text': xmlAttributes },
192
+ },
193
+ )
194
+ }
195
+
196
+ async internalStateUpdate(value) {
197
+ const prevState = this.service.getCharacteristic(this.hapChar.Active).value
198
+ try {
199
+ // Don't continue if the state is the same as before
200
+ if (value === prevState) {
201
+ return
202
+ }
203
+
204
+ // We also want to update the mode (by rotation speed)
205
+ let newRotSpeed = 0
206
+ if (value !== 0) {
207
+ // If turning on then we want to show the last used mode (by rotation speed)
208
+ switch (this.accessory.context.cacheLastOnMode) {
209
+ case 2:
210
+ newRotSpeed = 99
211
+ break
212
+ case 3:
213
+ newRotSpeed = 66
214
+ break
215
+ default:
216
+ newRotSpeed = 33
217
+ }
218
+ }
219
+
220
+ // Update the rotation speed, use setCharacteristic so the set handler is run to send updates
221
+ this.service.setCharacteristic(this.hapChar.RotationSpeed, newRotSpeed)
222
+ } catch (err) {
223
+ const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach])
224
+ this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`)
225
+
226
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
227
+ setTimeout(() => {
228
+ this.service.updateCharacteristic(this.hapChar.Active, prevState)
229
+ }, 2000)
230
+ throw new this.hapErr(-70402)
231
+ }
232
+ }
233
+
234
+ async internalModeUpdate(value) {
235
+ const prevSpeed = this.service.getCharacteristic(this.hapChar.RotationSpeed).value
236
+ try {
237
+ // Avoid multiple updates in quick succession
238
+ const updateKeyMode = generateRandomString(5)
239
+ this.updateKeyMode = updateKeyMode
240
+ await sleep(500)
241
+ if (updateKeyMode !== this.updateKeyMode) {
242
+ return
243
+ }
244
+
245
+ // Generate newValue for the needed mode and newSpeed in 33% multiples
246
+ let newValue = 1
247
+ let newSpeed = 0
248
+ if (value > 25 && value <= 50) {
249
+ newValue = 4
250
+ newSpeed = 33
251
+ } else if (value > 50 && value <= 75) {
252
+ newValue = 3
253
+ newSpeed = 66
254
+ } else if (value > 75) {
255
+ newValue = 2
256
+ newSpeed = 99
257
+ }
258
+
259
+ // Don't continue if the speed is the same as before
260
+ if (newSpeed === prevSpeed) {
261
+ return
262
+ }
263
+
264
+ // Send the update
265
+ await this.sendDeviceUpdate({
266
+ Mode: newValue,
267
+ SetTemperature: this.cToF[Number.parseInt(this.accessory.context.cacheLastOnTemp, 10)],
268
+ })
269
+
270
+ // Update the cache last used mode if not turning off
271
+ if (newValue !== 1) {
272
+ this.accessory.context.cacheLastOnMode = newValue
273
+ }
274
+
275
+ // Log the new mode if appropriate
276
+ this.accessory.log(`${platformLang.curMode} [${this.modeLabels[newValue]}]`)
277
+ } catch (err) {
278
+ const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach])
279
+ this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`)
280
+
281
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
282
+ setTimeout(() => {
283
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, prevSpeed)
284
+ }, 2000)
285
+ throw new this.hapErr(-70402)
286
+ }
287
+ }
288
+
289
+ async internalTargetTempUpdate(value) {
290
+ const prevTemp = this.service.getCharacteristic(this.hapChar.HeatingThresholdTemperature).value
291
+ try {
292
+ // Avoid multiple updates in quick succession
293
+ const updateKeyTemp = generateRandomString(5)
294
+ this.updateKeyTemp = updateKeyTemp
295
+ await sleep(500)
296
+ if (updateKeyTemp !== this.updateKeyTemp) {
297
+ return
298
+ }
299
+
300
+ // We want an integer target temp value and to not continue if this is the same as before
301
+ value = Number.parseInt(value, 10)
302
+ if (value === prevTemp) {
303
+ return
304
+ }
305
+
306
+ // Send the update
307
+ await this.sendDeviceUpdate({ SetTemperature: this.cToF[value] })
308
+
309
+ // Update the cache and log if appropriate
310
+ this.accessory.context.cacheLastOnTemp = value
311
+ this.accessory.log(`${platformLang.tarTemp} [${value}°C]`)
312
+ } catch (err) {
313
+ const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach])
314
+ this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`)
315
+
316
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
317
+ setTimeout(() => {
318
+ this.service.updateCharacteristic(this.hapChar.HeatingThresholdTemperature, prevTemp)
319
+ }, 2000)
320
+ throw new this.hapErr(-70402)
321
+ }
322
+ }
323
+
324
+ externalModeUpdate(value) {
325
+ try {
326
+ // We want to find a rotation speed based on the given mode
327
+ let rotSpeed = 0
328
+ switch (value) {
329
+ case 2: {
330
+ rotSpeed = 99
331
+ break
332
+ }
333
+ case 3: {
334
+ rotSpeed = 66
335
+ break
336
+ }
337
+ case 4: {
338
+ rotSpeed = 33
339
+ break
340
+ }
341
+ default:
342
+ return
343
+ }
344
+
345
+ // Update the HomeKit characteristics
346
+ this.service.updateCharacteristic(this.hapChar.Active, value !== 1 ? 1 : 0)
347
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, rotSpeed)
348
+
349
+ // Update the last used mode if the device is not off
350
+ if (value !== 1) {
351
+ this.accessory.context.cacheLastOnMode = value
352
+ }
353
+
354
+ // Log the change of mode if appropriate
355
+ this.accessory.log(`${platformLang.curMode} [${this.modeLabels[value]}]`)
356
+ } catch (err) {
357
+ this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`)
358
+ }
359
+ }
360
+
361
+ externalTargetTempUpdate(value) {
362
+ try {
363
+ // Don't continue if receiving frost-protect temperature (°C or °F)
364
+ if (value === 4 || value === 40) {
365
+ return
366
+ }
367
+
368
+ // A value greater than 50 normally means °F, so convert to °C
369
+ if (value > 50) {
370
+ value = Math.round(((value - 32) * 5) / 9)
371
+ }
372
+
373
+ // Make sure the value is in the [16, 29] range
374
+ value = Math.max(Math.min(value, 29), 16)
375
+
376
+ // Check if the new target temperature is different from the current target temperature
377
+ if (
378
+ this.service.getCharacteristic(this.hapChar.HeatingThresholdTemperature).value !== value
379
+ ) {
380
+ // Update the target temperature HomeKit characteristic
381
+ this.service.updateCharacteristic(this.hapChar.HeatingThresholdTemperature, value)
382
+
383
+ // Log the change if appropriate
384
+ this.accessory.log(`${platformLang.tarTemp} [${value}°C]`)
385
+ }
386
+
387
+ // Update the last-ON-target-temp cache
388
+ this.accessory.context.cacheLastOnTemp = value
389
+ } catch (err) {
390
+ this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`)
391
+ }
392
+ }
393
+
394
+ externalCurrentTempUpdate(value) {
395
+ try {
396
+ // A value greater than 50 normally means °F, so convert to °C
397
+ if (value > 50) {
398
+ value = Math.round(((value - 32) * 5) / 9)
399
+ }
400
+
401
+ // Don't continue if new current temperature is the same as before
402
+ if (this.cacheTemp === value) {
403
+ return
404
+ }
405
+
406
+ // Update the current temperature HomeKit characteristic
407
+ this.service.updateCharacteristic(this.hapChar.CurrentTemperature, value)
408
+
409
+ // Update the cache and log the change if appropriate
410
+ this.cacheTemp = value
411
+ this.accessory.log(`${platformLang.curTemp} [${this.cacheTemp}°C]`)
412
+ } catch (err) {
413
+ this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`)
414
+ }
415
+ }
416
+ }