@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
|
@@ -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
|
+
}
|