@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
package/eslint.config.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { antfu } from '@antfu/eslint-config'
|
|
2
|
+
|
|
3
|
+
/** @type {typeof antfu} */
|
|
4
|
+
export default antfu(
|
|
5
|
+
{
|
|
6
|
+
ignores: [],
|
|
7
|
+
jsx: false,
|
|
8
|
+
rules: {
|
|
9
|
+
'curly': ['error', 'multi-line'],
|
|
10
|
+
'new-cap': 'off',
|
|
11
|
+
'import/extensions': ['error', 'ignorePackages'],
|
|
12
|
+
'import/order': 0,
|
|
13
|
+
'jsdoc/check-alignment': 'warn',
|
|
14
|
+
'jsdoc/check-line-alignment': 'warn',
|
|
15
|
+
'jsdoc/require-returns-check': 0,
|
|
16
|
+
'jsdoc/require-returns-description': 0,
|
|
17
|
+
'no-undef': 'error',
|
|
18
|
+
'perfectionist/sort-exports': 'error',
|
|
19
|
+
'perfectionist/sort-imports': [
|
|
20
|
+
'error',
|
|
21
|
+
{
|
|
22
|
+
groups: [
|
|
23
|
+
'type',
|
|
24
|
+
'internal-type',
|
|
25
|
+
'builtin',
|
|
26
|
+
'external',
|
|
27
|
+
'internal',
|
|
28
|
+
['parent-type', 'sibling-type', 'index-type'],
|
|
29
|
+
['parent', 'sibling', 'index'],
|
|
30
|
+
'object',
|
|
31
|
+
'unknown',
|
|
32
|
+
],
|
|
33
|
+
order: 'asc',
|
|
34
|
+
type: 'natural',
|
|
35
|
+
newlinesBetween: 'always',
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
'perfectionist/sort-named-exports': 'error',
|
|
39
|
+
'perfectionist/sort-named-imports': 'error',
|
|
40
|
+
'quotes': ['error', 'single'],
|
|
41
|
+
'sort-imports': 0,
|
|
42
|
+
'style/brace-style': ['error', '1tbs', { allowSingleLine: true }],
|
|
43
|
+
'style/quote-props': ['error', 'consistent-as-needed'],
|
|
44
|
+
'test/no-only-tests': 'error',
|
|
45
|
+
'unicorn/no-useless-spread': 'error',
|
|
46
|
+
'unused-imports/no-unused-vars': ['error', { caughtErrors: 'none' }],
|
|
47
|
+
},
|
|
48
|
+
typescript: false,
|
|
49
|
+
},
|
|
50
|
+
)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import PQueue from 'p-queue'
|
|
3
|
+
import { parseStringPromise } from 'xml2js'
|
|
4
|
+
import xmlbuilder from 'xmlbuilder'
|
|
5
|
+
|
|
6
|
+
import { decodeXML, hasProperty, parseError } from '../utils/functions.js'
|
|
7
|
+
import platformLang from '../utils/lang-en.js'
|
|
8
|
+
|
|
9
|
+
export default class {
|
|
10
|
+
constructor(platform) {
|
|
11
|
+
// Set up global vars from the platform
|
|
12
|
+
this.platform = platform
|
|
13
|
+
this.queue = new PQueue({
|
|
14
|
+
concurrency: 1,
|
|
15
|
+
interval: 250,
|
|
16
|
+
intervalCap: 1,
|
|
17
|
+
timeout: 9000,
|
|
18
|
+
throwOnTimeout: true,
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async sendDeviceUpdate(accessory, serviceType, action, body) {
|
|
23
|
+
try {
|
|
24
|
+
return await this.queue.add(async () => {
|
|
25
|
+
// Check the device has this service (it should have)
|
|
26
|
+
if (
|
|
27
|
+
!accessory.context.serviceList[serviceType]
|
|
28
|
+
|| !accessory.context.serviceList[serviceType].controlURL
|
|
29
|
+
) {
|
|
30
|
+
throw new Error(platformLang.noService)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Generate the XML to send to the device
|
|
34
|
+
const xml = xmlbuilder
|
|
35
|
+
.create('s:Envelope', {
|
|
36
|
+
version: '1.0',
|
|
37
|
+
encoding: 'utf-8',
|
|
38
|
+
allowEmpty: true,
|
|
39
|
+
})
|
|
40
|
+
.att('xmlns:s', 'http://schemas.xmlsoap.org/soap/envelope/')
|
|
41
|
+
.att('s:encodingStyle', 'http://schemas.xmlsoap.org/soap/encoding/')
|
|
42
|
+
.ele('s:Body')
|
|
43
|
+
.ele(`u:${action}`)
|
|
44
|
+
.att('xmlns:u', serviceType)
|
|
45
|
+
|
|
46
|
+
// Send the request to the device
|
|
47
|
+
const hostPort = `http://${accessory.context.ipAddress}:${accessory.context.port}`
|
|
48
|
+
const res = await axios({
|
|
49
|
+
url: hostPort + accessory.context.serviceList[serviceType].controlURL,
|
|
50
|
+
method: 'post',
|
|
51
|
+
headers: {
|
|
52
|
+
'SOAPACTION': `"${serviceType}#${action}"`,
|
|
53
|
+
'Content-Type': 'text/xml; charset="utf-8"',
|
|
54
|
+
},
|
|
55
|
+
data: (body ? xml.ele(body) : xml).end(),
|
|
56
|
+
timeout: 10000,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Parse the response from the device
|
|
60
|
+
const xmlRes = res.data
|
|
61
|
+
const response = await parseStringPromise(xmlRes, {
|
|
62
|
+
explicitArray: false,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
if (!accessory.context.httpOnline) {
|
|
66
|
+
this.platform.updateHTTPStatus(accessory, true)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Return the parsed response
|
|
70
|
+
return response['s:Envelope']['s:Body'][`u:${action}Response`]
|
|
71
|
+
})
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const eText = parseError(err)
|
|
74
|
+
if (['at Object.<anonymous>', 'EHOSTUNREACH'].some(el => eText.includes(el))) {
|
|
75
|
+
// Device disconnected from network
|
|
76
|
+
if (accessory.context.httpOnline) {
|
|
77
|
+
this.platform.updateHTTPStatus(accessory, false)
|
|
78
|
+
}
|
|
79
|
+
throw new Error(
|
|
80
|
+
eText.includes('EHOSTUNREACH') ? platformLang.timeoutUnreach : platformLang.timeout,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
throw err
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async receiveDeviceUpdate(accessory, body) {
|
|
88
|
+
try {
|
|
89
|
+
accessory.logDebug(`${platformLang.incKnown}:\n${body.trim()}`)
|
|
90
|
+
|
|
91
|
+
// Convert the XML to JSON
|
|
92
|
+
const json = await parseStringPromise(body, { explicitArray: false })
|
|
93
|
+
|
|
94
|
+
// Loop through the JSON for the necessary information
|
|
95
|
+
|
|
96
|
+
for (const prop in json['e:propertyset']['e:property']) {
|
|
97
|
+
if (hasProperty(json['e:propertyset']['e:property'], prop)) {
|
|
98
|
+
const data = json['e:propertyset']['e:property'][prop]
|
|
99
|
+
switch (prop) {
|
|
100
|
+
case 'BinaryState':
|
|
101
|
+
try {
|
|
102
|
+
accessory.control.receiveDeviceUpdate({
|
|
103
|
+
name: 'BinaryState',
|
|
104
|
+
value: Number.parseInt(data.substring(0, 1), 10),
|
|
105
|
+
})
|
|
106
|
+
} catch (err) {
|
|
107
|
+
accessory.logWarn(`${prop} ${platformLang.proEr} ${parseError(err)}`)
|
|
108
|
+
}
|
|
109
|
+
break
|
|
110
|
+
case 'Brightness':
|
|
111
|
+
try {
|
|
112
|
+
accessory.control.receiveDeviceUpdate({
|
|
113
|
+
name: 'Brightness',
|
|
114
|
+
value: Number.parseInt(data, 10),
|
|
115
|
+
})
|
|
116
|
+
} catch (err) {
|
|
117
|
+
accessory.logWarn(`${prop} ${platformLang.proEr} ${parseError(err)}`)
|
|
118
|
+
}
|
|
119
|
+
break
|
|
120
|
+
case 'InsightParams': {
|
|
121
|
+
try {
|
|
122
|
+
const params = data.split('|')
|
|
123
|
+
accessory.control.receiveDeviceUpdate({
|
|
124
|
+
name: 'InsightParams',
|
|
125
|
+
value: {
|
|
126
|
+
state: Number.parseInt(params[0], 10),
|
|
127
|
+
power: Number.parseInt(params[7], 10),
|
|
128
|
+
todayWm: Number.parseFloat(params[8]),
|
|
129
|
+
todayOnSeconds: Number.parseFloat(params[3]),
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
} catch (err) {
|
|
133
|
+
accessory.logWarn(`${prop} ${platformLang.proEr} ${parseError(err)}`)
|
|
134
|
+
}
|
|
135
|
+
break
|
|
136
|
+
}
|
|
137
|
+
case 'attributeList':
|
|
138
|
+
try {
|
|
139
|
+
const decoded = decodeXML(data)
|
|
140
|
+
const xml = `<attributeList>${decoded}</attributeList>`
|
|
141
|
+
|
|
142
|
+
const result = await parseStringPromise(xml, { explicitArray: true })
|
|
143
|
+
result.attributeList.attribute.forEach((attribute) => {
|
|
144
|
+
accessory.control.receiveDeviceUpdate({
|
|
145
|
+
name: attribute.name[0],
|
|
146
|
+
value: Number.parseInt(attribute.value[0], 10),
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
} catch (err) {
|
|
150
|
+
accessory.logWarn(`${prop} ${platformLang.proEr} ${parseError(err)}`)
|
|
151
|
+
}
|
|
152
|
+
break
|
|
153
|
+
case 'StatusChange':
|
|
154
|
+
try {
|
|
155
|
+
const xml = await parseStringPromise(data, { explicitArray: false })
|
|
156
|
+
accessory.control.receiveDeviceUpdate(xml.StateEvent.DeviceID._, {
|
|
157
|
+
name: xml.StateEvent.CapabilityId,
|
|
158
|
+
value: xml.StateEvent.Value,
|
|
159
|
+
})
|
|
160
|
+
} catch (err) {
|
|
161
|
+
accessory.logWarn(`${prop} ${platformLang.proEr} ${parseError(err)}`)
|
|
162
|
+
}
|
|
163
|
+
break
|
|
164
|
+
default:
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch (err) {
|
|
170
|
+
// Catch any errors during this process
|
|
171
|
+
accessory.logWarn(`${platformLang.incFail} ${parseError(err)}`)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { request } from 'node:http'
|
|
2
|
+
|
|
3
|
+
import platformConsts from '../utils/constants.js'
|
|
4
|
+
import { parseError } from '../utils/functions.js'
|
|
5
|
+
import platformLang from '../utils/lang-en.js'
|
|
6
|
+
|
|
7
|
+
export default class {
|
|
8
|
+
constructor(platform, accessory) {
|
|
9
|
+
// Set up global vars from the platform
|
|
10
|
+
this.platform = platform
|
|
11
|
+
this.upnpInterval = platform.config.upnpInterval
|
|
12
|
+
this.upnpIntervalMilli = this.upnpInterval * 1000
|
|
13
|
+
|
|
14
|
+
// Set up other variables we need
|
|
15
|
+
this.accessory = accessory
|
|
16
|
+
this.services = accessory.context.serviceList
|
|
17
|
+
this.subs = {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
startSubscriptions() {
|
|
21
|
+
// Subscribe to each of the services that the device supports, that the plugin uses
|
|
22
|
+
Object.keys(this.services)
|
|
23
|
+
.filter(el => platformConsts.servicesToSubscribe.includes(el))
|
|
24
|
+
.forEach((service) => {
|
|
25
|
+
// Subscript to the service
|
|
26
|
+
this.subs[service] = {}
|
|
27
|
+
this.subscribe(service)
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
stopSubscriptions() {
|
|
32
|
+
Object.entries(this.subs).forEach((entry) => {
|
|
33
|
+
const [serviceType, sub] = entry
|
|
34
|
+
if (sub.timeout) {
|
|
35
|
+
clearTimeout(sub.timeout)
|
|
36
|
+
}
|
|
37
|
+
if (sub.status) {
|
|
38
|
+
this.unsubscribe(serviceType)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
this.accessory.logDebugWarn(platformLang.stoppedSubs)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
subscribe(serviceType) {
|
|
45
|
+
try {
|
|
46
|
+
// Check to see an already sent request is still pending
|
|
47
|
+
if (this.subs[serviceType].status === 'PENDING') {
|
|
48
|
+
this.accessory.logDebug(`[${serviceType}] ${platformLang.subPending}`)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Set up the options for the subscription request
|
|
53
|
+
const timeout = this.upnpInterval + 10
|
|
54
|
+
const options = {
|
|
55
|
+
host: this.accessory.context.ipAddress,
|
|
56
|
+
port: this.accessory.context.port,
|
|
57
|
+
path: this.services[serviceType].eventSubURL,
|
|
58
|
+
method: 'SUBSCRIBE',
|
|
59
|
+
headers: { TIMEOUT: `Second-${timeout}` },
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The remaining options depend on whether the subscription already exists
|
|
63
|
+
if (this.subs[serviceType].status) {
|
|
64
|
+
// Subscription already exists so renew
|
|
65
|
+
options.headers.SID = this.subs[serviceType].status
|
|
66
|
+
} else {
|
|
67
|
+
// Subscription doesn't exist yet to set up for new subscription
|
|
68
|
+
this.subs[serviceType].status = 'PENDING'
|
|
69
|
+
this.accessory.logDebug(`[${serviceType}] ${platformLang.subInit}`)
|
|
70
|
+
options.headers.CALLBACK = `<http://${this.accessory.context.cbURL}/${this.accessory.UUID}>`
|
|
71
|
+
options.headers.NT = 'upnp:event'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Execute the subscription request
|
|
75
|
+
const req = request(options, (res) => {
|
|
76
|
+
if (res.statusCode === 200) {
|
|
77
|
+
// Subscription request successful
|
|
78
|
+
this.subs[serviceType].status = res.headers.sid
|
|
79
|
+
|
|
80
|
+
// Renew subscription after 150 seconds
|
|
81
|
+
this.subs[serviceType].timeout = setTimeout(
|
|
82
|
+
() => this.subscribe(serviceType),
|
|
83
|
+
this.upnpIntervalMilli,
|
|
84
|
+
)
|
|
85
|
+
} else {
|
|
86
|
+
// Subscription request failure
|
|
87
|
+
this.accessory.logDebugWarn(`[${serviceType}] ${platformLang.subError} [${res.statusCode}]`)
|
|
88
|
+
this.subs[serviceType].status = null
|
|
89
|
+
|
|
90
|
+
// Try to recover from a failed subscription after 10 seconds
|
|
91
|
+
this.subs[serviceType].timeout = setTimeout(() => this.subscribe(serviceType), 10000)
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Listen for errors on the subscription
|
|
96
|
+
req.removeAllListeners('error')
|
|
97
|
+
req.on('error', (err) => {
|
|
98
|
+
if (!err) {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Stop the subscriptions
|
|
103
|
+
this.stopSubscriptions()
|
|
104
|
+
|
|
105
|
+
// Use the platform function to disable the upnp client for this accessory
|
|
106
|
+
this.platform.disableUPNP(this.accessory, err)
|
|
107
|
+
})
|
|
108
|
+
req.end()
|
|
109
|
+
} catch (err) {
|
|
110
|
+
// Catch any errors during the process
|
|
111
|
+
this.accessory.logDebugWarn(`[${serviceType}] ${platformLang.subscribeError} ${parseError(err)}`)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
unsubscribe(serviceType) {
|
|
116
|
+
try {
|
|
117
|
+
// Check to see an already sent request is still pending
|
|
118
|
+
if (!this.subs[serviceType] || this.subs[serviceType].status === 'PENDING') {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Set up the options for the subscription request
|
|
123
|
+
const options = {
|
|
124
|
+
host: this.accessory.context.ipAddress,
|
|
125
|
+
port: this.accessory.context.port,
|
|
126
|
+
path: this.services[serviceType].eventSubURL,
|
|
127
|
+
method: 'UNSUBSCRIBE',
|
|
128
|
+
headers: { SID: this.subs[serviceType].status },
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Execute the subscription request
|
|
132
|
+
const req = request(options, (res) => {
|
|
133
|
+
if (res.statusCode === 200) {
|
|
134
|
+
// Unsubscribed
|
|
135
|
+
this.accessory.logDebug(`[${serviceType}] unsubscribe successful`)
|
|
136
|
+
} else {
|
|
137
|
+
// Subscription request failure
|
|
138
|
+
this.accessory.logDebugWarn(`[${serviceType}] ${platformLang.unsubFail} [${res.statusCode}]`)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// Listen for errors on the subscription
|
|
143
|
+
req.removeAllListeners('error')
|
|
144
|
+
req.on('error', (err) => {
|
|
145
|
+
if (!err) {
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
this.accessory.logDebugWarn(`[${serviceType}] ${platformLang.unsubFail} [${err.message}]`)
|
|
149
|
+
})
|
|
150
|
+
req.end()
|
|
151
|
+
} catch (err) {
|
|
152
|
+
this.accessory.logDebugWarn(`[${serviceType}] ${platformLang.unsubFail} [${parseError(err)}]`)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Builder, parseStringPromise } from 'xml2js'
|
|
2
|
+
|
|
3
|
+
import { decodeXML, parseError } from '../utils/functions.js'
|
|
4
|
+
import platformLang from '../utils/lang-en.js'
|
|
5
|
+
|
|
6
|
+
export default class {
|
|
7
|
+
constructor(platform, accessory) {
|
|
8
|
+
// Set up variables from the platform
|
|
9
|
+
this.hapChar = platform.api.hap.Characteristic
|
|
10
|
+
this.hapErr = platform.api.hap.HapStatusError
|
|
11
|
+
this.hapServ = platform.api.hap.Service
|
|
12
|
+
this.platform = platform
|
|
13
|
+
|
|
14
|
+
// Set up variables from the accessory
|
|
15
|
+
this.accessory = accessory
|
|
16
|
+
|
|
17
|
+
// Add the switch service if it doesn't already exist
|
|
18
|
+
this.service = this.accessory.getService(this.hapServ.Switch)
|
|
19
|
+
|| this.accessory.addService(this.hapServ.Switch)
|
|
20
|
+
|
|
21
|
+
// Add the set handler to the outlet on/off characteristic
|
|
22
|
+
this.service
|
|
23
|
+
.getCharacteristic(this.hapChar.On)
|
|
24
|
+
.removeOnSet()
|
|
25
|
+
.onSet(async value => this.internalModeUpdate(value))
|
|
26
|
+
|
|
27
|
+
// Output the customised options to the log
|
|
28
|
+
const opts = JSON.stringify({
|
|
29
|
+
})
|
|
30
|
+
platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
|
|
31
|
+
|
|
32
|
+
// Request a device update immediately
|
|
33
|
+
this.requestDeviceUpdate()
|
|
34
|
+
|
|
35
|
+
// Start a polling interval if the user has disabled upnp
|
|
36
|
+
if (this.accessory.context.connection === 'http') {
|
|
37
|
+
this.pollingInterval = setInterval(
|
|
38
|
+
() => this.requestDeviceUpdate(),
|
|
39
|
+
platform.config.pollingInterval * 1000,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
receiveDeviceUpdate(attribute) {
|
|
45
|
+
// Log the receiving update if debug is enabled
|
|
46
|
+
this.accessory.logDebug(`${platformLang.recUpd} [${attribute.name}: ${JSON.stringify(attribute.value)}]`)
|
|
47
|
+
|
|
48
|
+
// Check which attribute we are getting
|
|
49
|
+
switch (attribute.name) {
|
|
50
|
+
case 'Mode':
|
|
51
|
+
this.externalModeUpdate(attribute.value)
|
|
52
|
+
break
|
|
53
|
+
default:
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async sendDeviceUpdate(attributes) {
|
|
58
|
+
// Log the sending update if debug is enabled
|
|
59
|
+
this.accessory.log(`${platformLang.senUpd} ${JSON.stringify(attributes)}`)
|
|
60
|
+
|
|
61
|
+
// Generate the XML to send
|
|
62
|
+
const builder = new Builder({
|
|
63
|
+
rootName: 'attribute',
|
|
64
|
+
headless: true,
|
|
65
|
+
renderOpts: { pretty: false },
|
|
66
|
+
})
|
|
67
|
+
const xmlAttributes = Object.keys(attributes)
|
|
68
|
+
.map(attributeKey => builder.buildObject({
|
|
69
|
+
name: attributeKey,
|
|
70
|
+
value: attributes[attributeKey],
|
|
71
|
+
}))
|
|
72
|
+
.join('')
|
|
73
|
+
|
|
74
|
+
// Send the update
|
|
75
|
+
await this.platform.httpClient.sendDeviceUpdate(
|
|
76
|
+
this.accessory,
|
|
77
|
+
'urn:Belkin:service:deviceevent:1',
|
|
78
|
+
'SetAttributes',
|
|
79
|
+
{
|
|
80
|
+
attributeList: { '#text': xmlAttributes },
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async requestDeviceUpdate() {
|
|
86
|
+
try {
|
|
87
|
+
// Request the update
|
|
88
|
+
const data = await this.platform.httpClient.sendDeviceUpdate(
|
|
89
|
+
this.accessory,
|
|
90
|
+
'urn:Belkin:service:deviceevent:1',
|
|
91
|
+
'GetAttributes',
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
// Parse the response
|
|
95
|
+
const decoded = decodeXML(data.attributeList)
|
|
96
|
+
const xml = `<attributeList>${decoded}</attributeList>`
|
|
97
|
+
const result = await parseStringPromise(xml, { explicitArray: false })
|
|
98
|
+
Object.keys(result.attributeList.attribute).forEach((key) => {
|
|
99
|
+
// Only send the required attributes to the receiveDeviceUpdate function
|
|
100
|
+
switch (result.attributeList.attribute[key].name) {
|
|
101
|
+
case 'Mode':
|
|
102
|
+
this.receiveDeviceUpdate({
|
|
103
|
+
name: result.attributeList.attribute[key].name,
|
|
104
|
+
value: Number.parseInt(result.attributeList.attribute[key].value, 10),
|
|
105
|
+
})
|
|
106
|
+
break
|
|
107
|
+
default:
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
} catch (err) {
|
|
111
|
+
const eText = parseError(err, [
|
|
112
|
+
platformLang.timeout,
|
|
113
|
+
platformLang.timeoutUnreach,
|
|
114
|
+
platformLang.noService,
|
|
115
|
+
])
|
|
116
|
+
this.accessory.logDebugWarn(`${platformLang.rduErr} ${eText}`)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async internalModeUpdate(value) {
|
|
121
|
+
try {
|
|
122
|
+
// Coffee maker cannot be turned off remotely
|
|
123
|
+
if (!value) {
|
|
124
|
+
throw new Error('coffee maker cannot be turned off remotely')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Send the update to turn ON
|
|
128
|
+
await this.sendDeviceUpdate({ Mode: 4 })
|
|
129
|
+
|
|
130
|
+
// Update the cache value and log the change if appropriate
|
|
131
|
+
this.cacheState = true
|
|
132
|
+
this.accessory.log(`${platformLang.curState} [on]`)
|
|
133
|
+
} catch (err) {
|
|
134
|
+
// Catch any errors
|
|
135
|
+
const eText = parseError(err, [platformLang.timeout, platformLang.timeoutUnreach])
|
|
136
|
+
this.accessory.logWarn(`${platformLang.cantCtl} ${eText}`)
|
|
137
|
+
|
|
138
|
+
// Throw a 'no response' error and set a timeout to revert this after 2 seconds
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
this.service.updateCharacteristic(this.hapChar.On, this.cacheState)
|
|
141
|
+
}, 2000)
|
|
142
|
+
throw new this.hapErr(-70402)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
externalModeUpdate(value) {
|
|
147
|
+
try {
|
|
148
|
+
// Value of 4 means brewing (ON) otherwise (OFF)
|
|
149
|
+
value = value === 4
|
|
150
|
+
|
|
151
|
+
// Check to see if the cache value is different
|
|
152
|
+
if (value === this.cacheState) {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Update the HomeKit characteristics
|
|
157
|
+
this.service.updateCharacteristic(this.hapChar.On, value)
|
|
158
|
+
|
|
159
|
+
// Update the cache value and log the change if appropriate
|
|
160
|
+
this.cacheState = value
|
|
161
|
+
this.accessory.log(`${platformLang.curState} [${value ? 'on' : 'off'}]`)
|
|
162
|
+
} catch (err) {
|
|
163
|
+
// Catch any errors
|
|
164
|
+
this.accessory.logWarn(`${platformLang.cantUpd} ${parseError(err)}`)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|