@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,379 @@
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 humidifier service if it doesn't already exist
23
+ this.service = this.accessory.getService(this.hapServ.HumidifierDehumidifier)
24
+ || this.accessory.addService(this.hapServ.HumidifierDehumidifier)
25
+
26
+ // Add the set handler to the humidifier 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 humidifier target state characteristic
33
+ this.service
34
+ .getCharacteristic(this.hapChar.TargetHumidifierDehumidifierState)
35
+ .updateValue(1)
36
+ .setProps({
37
+ minValue: 1,
38
+ maxValue: 1,
39
+ validValues: [1],
40
+ })
41
+
42
+ // Add the set handler to the humidifier target relative humidity characteristic
43
+ this.service
44
+ .getCharacteristic(this.hapChar.RelativeHumidityHumidifierThreshold)
45
+ .onSet(async (value) => {
46
+ await this.internalTargetHumidityUpdate(value)
47
+ })
48
+
49
+ // Add the set handler to the humidifier target state characteristic
50
+ this.service
51
+ .getCharacteristic(this.hapChar.RotationSpeed)
52
+ .setProps({ minStep: 20 })
53
+ .onSet(async (value) => {
54
+ await this.internalModeUpdate(value)
55
+ })
56
+
57
+ // Add a last mode cache value if not already set
58
+ const cacheMode = this.accessory.context.cacheLastOnMode
59
+ if (!cacheMode || cacheMode === 0) {
60
+ this.accessory.context.cacheLastOnMode = 1
61
+ }
62
+
63
+ // Some conversion objects
64
+ this.modeLabels = {
65
+ 0: platformLang.labelOff,
66
+ 1: platformLang.labelMin,
67
+ 2: platformLang.labelLow,
68
+ 3: platformLang.labelMed,
69
+ 4: platformLang.labelHigh,
70
+ 5: platformLang.labelMax,
71
+ }
72
+ this.hToWemoFormat = {
73
+ 45: 0,
74
+ 50: 1,
75
+ 55: 2,
76
+ 60: 3,
77
+ 100: 4,
78
+ }
79
+ this.wemoFormatToH = {
80
+ 0: 45,
81
+ 1: 50,
82
+ 2: 55,
83
+ 3: 60,
84
+ 4: 100,
85
+ }
86
+
87
+ // Output the customised options to the log
88
+ const opts = JSON.stringify({
89
+ })
90
+ platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
91
+
92
+ // Request a device update immediately
93
+ this.requestDeviceUpdate()
94
+
95
+ // Start a polling interval if the user has disabled upnp
96
+ if (this.accessory.context.connection === 'http') {
97
+ this.pollingInterval = setInterval(
98
+ () => this.requestDeviceUpdate(),
99
+ platform.config.pollingInterval * 1000,
100
+ )
101
+ }
102
+ }
103
+
104
+ receiveDeviceUpdate(attribute) {
105
+ // Log the receiving update if debug is enabled
106
+ this.accessory.logDebug(`${platformLang.recUpd} [${attribute.name}: ${JSON.stringify(attribute.value)}]`)
107
+
108
+ // Check which attribute we are getting
109
+ switch (attribute.name) {
110
+ case 'FanMode':
111
+ this.externalModeUpdate(attribute.value)
112
+ break
113
+ case 'CurrentHumidity':
114
+ this.externalCurrentHumidityUpdate(attribute.value)
115
+ break
116
+ case 'DesiredHumidity':
117
+ this.externalTargetHumidityUpdate(attribute.value)
118
+ break
119
+ default:
120
+ }
121
+ }
122
+
123
+ async requestDeviceUpdate() {
124
+ try {
125
+ // Request the update
126
+ const data = await this.platform.httpClient.sendDeviceUpdate(
127
+ this.accessory,
128
+ 'urn:Belkin:service:deviceevent:1',
129
+ 'GetAttributes',
130
+ )
131
+
132
+ // Parse the response
133
+ const decoded = decodeXML(data.attributeList)
134
+ const xml = `<attributeList>${decoded}</attributeList>`
135
+ const result = await parseStringPromise(xml, { explicitArray: false })
136
+ Object.keys(result.attributeList.attribute).forEach((key) => {
137
+ // Only send the required attributes to the receiveDeviceUpdate function
138
+ switch (result.attributeList.attribute[key].name) {
139
+ case 'FanMode':
140
+ case 'CurrentHumidity':
141
+ case 'DesiredHumidity':
142
+ this.receiveDeviceUpdate({
143
+ name: result.attributeList.attribute[key].name,
144
+ value: Number.parseInt(result.attributeList.attribute[key].value, 10),
145
+ })
146
+ break
147
+ default:
148
+ }
149
+ })
150
+ } catch (err) {
151
+ const eText = parseError(err, [
152
+ platformLang.timeout,
153
+ platformLang.timeoutUnreach,
154
+ platformLang.noService,
155
+ ])
156
+ this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`)
157
+ }
158
+ }
159
+
160
+ async sendDeviceUpdate(attributes) {
161
+ // Log the sending update if debug is enabled
162
+ this.accessory.log(`${platformLang.senUpd} ${JSON.stringify(attributes)}`)
163
+
164
+ // Generate the XML to send
165
+ const builder = new Builder({
166
+ rootName: 'attribute',
167
+ headless: true,
168
+ renderOpts: { pretty: false },
169
+ })
170
+ const xmlAttributes = Object.keys(attributes)
171
+ .map(attributeKey => builder.buildObject({
172
+ name: attributeKey,
173
+ value: attributes[attributeKey],
174
+ }))
175
+ .join('')
176
+
177
+ // Send the update
178
+ await this.platform.httpClient.sendDeviceUpdate(
179
+ this.accessory,
180
+ 'urn:Belkin:service:deviceevent:1',
181
+ 'SetAttributes',
182
+ {
183
+ attributeList: { '#text': xmlAttributes },
184
+ },
185
+ )
186
+ }
187
+
188
+ async internalStateUpdate(value) {
189
+ const prevState = this.service.getCharacteristic(this.hapChar.Active).value
190
+ try {
191
+ // Don't continue if the state is the same as before
192
+ if (value === prevState) {
193
+ return
194
+ }
195
+
196
+ // We also want to update the mode by rotation speed when turning on/off
197
+ // Use the set handler to run the RotationSpeed set handler, to send updates to device
198
+ this.service.setCharacteristic(
199
+ this.hapChar.RotationSpeed,
200
+ value === 0 ? 0 : this.accessory.context.cacheLastOnMode * 20,
201
+ )
202
+ } catch (err) {
203
+ const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach])
204
+ this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`)
205
+
206
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
207
+ setTimeout(() => {
208
+ this.service.updateCharacteristic(this.hapChar.Active, prevState)
209
+ }, 2000)
210
+ throw new this.hapErr(-70402)
211
+ }
212
+ }
213
+
214
+ async internalModeUpdate(value) {
215
+ const prevSpeed = this.service.getCharacteristic(this.hapChar.RotationSpeed).value
216
+ try {
217
+ // Avoid multiple updates in quick succession
218
+ const updateKeyMode = generateRandomString(5)
219
+ this.updateKeyMode = updateKeyMode
220
+ await sleep(500)
221
+ if (updateKeyMode !== this.updateKeyMode) {
222
+ return
223
+ }
224
+
225
+ // Find the new needed mode from the given rotation speed
226
+ let newValue = 0
227
+ if (value > 10 && value <= 30) {
228
+ newValue = 1
229
+ } else if (value > 30 && value <= 50) {
230
+ newValue = 2
231
+ } else if (value > 50 && value <= 70) {
232
+ newValue = 3
233
+ } else if (value > 70 && value <= 90) {
234
+ newValue = 4
235
+ } else if (value > 90) {
236
+ newValue = 5
237
+ }
238
+
239
+ // Don't continue if the rotation speed is the same as before
240
+ if (value === prevSpeed) {
241
+ return
242
+ }
243
+
244
+ // Send the update
245
+ await this.sendDeviceUpdate({
246
+ FanMode: newValue.toString(),
247
+ })
248
+
249
+ // Update the last used mode cache if rotation speed is not 0
250
+ if (newValue !== 0) {
251
+ this.accessory.context.cacheLastOnMode = newValue
252
+ }
253
+
254
+ // Log the update if appropriate
255
+ this.accessory.log(`${platformLang.curMode} [${this.modeLabels[newValue]}]`)
256
+ } catch (err) {
257
+ const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach])
258
+ this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`)
259
+
260
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
261
+ setTimeout(() => {
262
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, prevSpeed)
263
+ }, 2000)
264
+ throw new this.hapErr(-70402)
265
+ }
266
+ }
267
+
268
+ async internalTargetHumidityUpdate(value) {
269
+ const prevHumi = this.service.getCharacteristic(
270
+ this.hapChar.RelativeHumidityHumidifierThreshold,
271
+ ).value
272
+ try {
273
+ // Avoid multiple updates in quick succession
274
+ const updateKeyHumi = generateRandomString(5)
275
+ this.updateKeyHumi = updateKeyHumi
276
+ await sleep(500)
277
+ if (updateKeyHumi !== this.updateKeyHumi) {
278
+ return
279
+ }
280
+
281
+ // Find the new target humidity mode from the target humidity given
282
+ let newValue = 45
283
+ if (value >= 47 && value < 52) {
284
+ newValue = 50
285
+ } else if (value >= 52 && value < 57) {
286
+ newValue = 55
287
+ } else if (value >= 57 && value < 80) {
288
+ newValue = 60
289
+ } else if (value >= 80) {
290
+ newValue = 100
291
+ }
292
+
293
+ // Don't continue if the new mode is the same as before
294
+ if (newValue === prevHumi) {
295
+ return
296
+ }
297
+
298
+ // Send the update
299
+ await this.sendDeviceUpdate({
300
+ DesiredHumidity: this.hToWemoFormat[newValue],
301
+ })
302
+
303
+ // Log the change if appropriate
304
+ this.accessory.log(`${platformLang.tarHumi} [${newValue}%]`)
305
+ } catch (err) {
306
+ const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach])
307
+ this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`)
308
+
309
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
310
+ setTimeout(() => {
311
+ this.service.updateCharacteristic(
312
+ this.hapChar.RelativeHumidityHumidifierThreshold,
313
+ prevHumi,
314
+ )
315
+ }, 2000)
316
+ throw new this.hapErr(-70402)
317
+ }
318
+ }
319
+
320
+ externalModeUpdate(value) {
321
+ try {
322
+ // Find the needed rotation speed from the given mode
323
+ const rotSpeed = value * 20
324
+
325
+ // Update the HomeKit characteristics
326
+ this.service.updateCharacteristic(this.hapChar.Active, value !== 0 ? 1 : 0)
327
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, rotSpeed)
328
+
329
+ // Update the last used mode if not off
330
+ if (value !== 0) {
331
+ this.accessory.context.cacheLastOnMode = value
332
+ }
333
+
334
+ // Log the change if appropriate
335
+ this.accessory.log(`${platformLang.curMode} [${this.modeLabels[value]}]`)
336
+ } catch (err) {
337
+ this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`)
338
+ }
339
+ }
340
+
341
+ externalTargetHumidityUpdate(value) {
342
+ try {
343
+ // Find the HomeKit value version from the given target humidity mode
344
+ value = this.wemoFormatToH[value]
345
+
346
+ // Don't continue if the new target is the same as the current target
347
+ const t = this.service.getCharacteristic(this.hapChar.RelativeHumidityHumidifierThreshold)
348
+ .value
349
+ if (t === value) {
350
+ return
351
+ }
352
+
353
+ // Update the target humidity HomeKit characteristics
354
+ this.service.updateCharacteristic(this.hapChar.RelativeHumidityHumidifierThreshold, value)
355
+
356
+ // Log the change if appropriate
357
+ this.accessory.log(`${platformLang.tarHumi} [${value}%]`)
358
+ } catch (err) {
359
+ this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`)
360
+ }
361
+ }
362
+
363
+ externalCurrentHumidityUpdate(value) {
364
+ try {
365
+ // Don't continue if the new current humidity is the same as before
366
+ if (this.service.getCharacteristic(this.hapChar.CurrentRelativeHumidity).value === value) {
367
+ return
368
+ }
369
+
370
+ // Update the current relative humidity HomeKit characteristic
371
+ this.service.updateCharacteristic(this.hapChar.CurrentRelativeHumidity, value)
372
+
373
+ // Log the change if appropriate
374
+ this.accessory.log(`${platformLang.curHumi} [${value}%]`)
375
+ } catch (err) {
376
+ this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`)
377
+ }
378
+ }
379
+ }
@@ -0,0 +1,39 @@
1
+ import deviceCoffee from './coffee.js'
2
+ import deviceCrockpot from './crockpot.js'
3
+ import deviceDimmer from './dimmer.js'
4
+ import deviceHeater from './heater.js'
5
+ import deviceHumidifier from './humidifier.js'
6
+ import deviceInsight from './insight.js'
7
+ import deviceLightSwitch from './lightswitch.js'
8
+ import deviceLinkBulb from './link-bulb.js'
9
+ import deviceLinkHub from './link-hub.js'
10
+ import deviceMakerGarage from './maker-garage.js'
11
+ import deviceMakerSwitch from './maker-switch.js'
12
+ import deviceMotion from './motion.js'
13
+ import deviceOutlet from './outlet.js'
14
+ import devicePurifier from './purifier.js'
15
+ import deviceSimPurifierInsight from './simulation/purifier-insight.js'
16
+ import deviceSimPurifier from './simulation/purifier.js'
17
+ import deviceSimSwitchInsight from './simulation/switch-insight.js'
18
+ import deviceSimSwitch from './simulation/switch.js'
19
+
20
+ export default {
21
+ deviceCoffee,
22
+ deviceCrockpot,
23
+ deviceDimmer,
24
+ deviceHeater,
25
+ deviceHumidifier,
26
+ deviceInsight,
27
+ deviceLightSwitch,
28
+ deviceLinkBulb,
29
+ deviceLinkHub,
30
+ deviceMakerGarage,
31
+ deviceMakerSwitch,
32
+ deviceMotion,
33
+ deviceOutlet,
34
+ devicePurifier,
35
+ deviceSimPurifierInsight,
36
+ deviceSimPurifier,
37
+ deviceSimSwitchInsight,
38
+ deviceSimSwitch,
39
+ }