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