@homebridge-plugins/homebridge-ewelink 12.3.3
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 +1363 -0
- package/LICENSE +21 -0
- package/README.md +68 -0
- package/config.schema.json +1757 -0
- package/eslint.config.js +15 -0
- package/lib/connection/api.js +508 -0
- package/lib/connection/http.js +304 -0
- package/lib/connection/lan.js +318 -0
- package/lib/connection/ws.js +414 -0
- package/lib/device/curtain.js +324 -0
- package/lib/device/diffuser.js +473 -0
- package/lib/device/fan.js +349 -0
- package/lib/device/group.js +106 -0
- package/lib/device/humidifier.js +199 -0
- package/lib/device/index.js +61 -0
- package/lib/device/light-cct.js +333 -0
- package/lib/device/light-dimmer.js +257 -0
- package/lib/device/light-rgb-cct.js +809 -0
- package/lib/device/light-rgb.js +272 -0
- package/lib/device/motor.js +363 -0
- package/lib/device/outlet-multi.js +417 -0
- package/lib/device/outlet-single-inched.js +409 -0
- package/lib/device/outlet-single.js +384 -0
- package/lib/device/panel.js +382 -0
- package/lib/device/rf-bridge.js +321 -0
- package/lib/device/rf-button.js +125 -0
- package/lib/device/rf-sensor.js +173 -0
- package/lib/device/sensor-ambient.js +311 -0
- package/lib/device/sensor-contact.js +140 -0
- package/lib/device/sensor-temp-humi.js +182 -0
- package/lib/device/simulation/blind.js +253 -0
- package/lib/device/simulation/cooler.js +312 -0
- package/lib/device/simulation/door.js +253 -0
- package/lib/device/simulation/doorbell.js +190 -0
- package/lib/device/simulation/garage-eachen.js +235 -0
- package/lib/device/simulation/garage-four.js +176 -0
- package/lib/device/simulation/garage-od-switch.js +36 -0
- package/lib/device/simulation/garage-one.js +395 -0
- package/lib/device/simulation/garage-two.js +394 -0
- package/lib/device/simulation/gate-one.js +327 -0
- package/lib/device/simulation/heater.js +389 -0
- package/lib/device/simulation/index.js +75 -0
- package/lib/device/simulation/light-fan.js +258 -0
- package/lib/device/simulation/lock-eachen.js +163 -0
- package/lib/device/simulation/lock-one.js +366 -0
- package/lib/device/simulation/p-button.js +223 -0
- package/lib/device/simulation/purifier.js +289 -0
- package/lib/device/simulation/rf-blind.js +192 -0
- package/lib/device/simulation/rf-door.js +180 -0
- package/lib/device/simulation/rf-window.js +180 -0
- package/lib/device/simulation/sensor-hidden.js +105 -0
- package/lib/device/simulation/sensor-leak.js +123 -0
- package/lib/device/simulation/sensor-visible.js +231 -0
- package/lib/device/simulation/sensor.js +300 -0
- package/lib/device/simulation/switch-valve.js +351 -0
- package/lib/device/simulation/tap-one.js +283 -0
- package/lib/device/simulation/tap-two.js +266 -0
- package/lib/device/simulation/th-cooler.js +443 -0
- package/lib/device/simulation/th-dehumidifier.js +424 -0
- package/lib/device/simulation/th-heater.js +449 -0
- package/lib/device/simulation/th-humidifier.js +424 -0
- package/lib/device/simulation/th-thermostat.js +611 -0
- package/lib/device/simulation/tv.js +252 -0
- package/lib/device/simulation/valve-four.js +232 -0
- package/lib/device/simulation/valve-one.js +345 -0
- package/lib/device/simulation/valve-two.js +340 -0
- package/lib/device/simulation/window.js +253 -0
- package/lib/device/switch-man.js +106 -0
- package/lib/device/switch-mate.js +82 -0
- package/lib/device/switch-multi.js +411 -0
- package/lib/device/switch-single-inched.js +332 -0
- package/lib/device/switch-single.js +307 -0
- package/lib/device/template.js +43 -0
- package/lib/device/thermostat.js +286 -0
- package/lib/device/zigbee/index.js +27 -0
- package/lib/device/zigbee/light-cct.js +263 -0
- package/lib/device/zigbee/light-dimmer.js +148 -0
- package/lib/device/zigbee/light-rgb-cct.js +400 -0
- package/lib/device/zigbee/motor.js +301 -0
- package/lib/device/zigbee/sensor-ambient.js +177 -0
- package/lib/device/zigbee/sensor-contact.js +137 -0
- package/lib/device/zigbee/sensor-leak.js +119 -0
- package/lib/device/zigbee/sensor-motion.js +119 -0
- package/lib/device/zigbee/sensor-occupancy.js +119 -0
- package/lib/device/zigbee/sensor-smoke.js +96 -0
- package/lib/device/zigbee/switch-stateless.js +104 -0
- package/lib/device/zigbee/thermostat.js +323 -0
- package/lib/fakegato/LICENSE +21 -0
- package/lib/fakegato/fakegato-history.js +813 -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 +365 -0
- package/lib/homebridge-ui/server.js +46 -0
- package/lib/index.js +7 -0
- package/lib/node-dns-sd/LICENSE +21 -0
- package/lib/node-dns-sd/README.md +613 -0
- package/lib/node-dns-sd/lib/dns-sd-classes.json +6 -0
- package/lib/node-dns-sd/lib/dns-sd-composer.js +83 -0
- package/lib/node-dns-sd/lib/dns-sd-parser.js +344 -0
- package/lib/node-dns-sd/lib/dns-sd-types.json +52 -0
- package/lib/node-dns-sd/lib/dns-sd.js +580 -0
- package/lib/platform.js +2156 -0
- package/lib/utils/colour.js +552 -0
- package/lib/utils/constants.js +589 -0
- package/lib/utils/custom-chars.js +33 -0
- package/lib/utils/eve-chars.js +130 -0
- package/lib/utils/functions.js +42 -0
- package/lib/utils/lang-en.js +160 -0
- package/lib/utils/lang-fr.js +163 -0
- package/lib/utils/lang-th.js +160 -0
- package/lib/utils/lang.js +5 -0
- package/package.json +75 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import { createHmac } from 'node:crypto'
|
|
3
|
+
import axios from 'axios'
|
|
4
|
+
import platformConsts from '../utils/constants.js'
|
|
5
|
+
import { sleep } from '../utils/functions.js'
|
|
6
|
+
|
|
7
|
+
export default class {
|
|
8
|
+
constructor(platform) {
|
|
9
|
+
// Set up variables from the platform
|
|
10
|
+
this.appId = platform.config.appId || platformConsts.appId
|
|
11
|
+
this.appSecret = platform.config.appSecret || platformConsts.appSecret
|
|
12
|
+
this.countryCode = platform.config.countryCode
|
|
13
|
+
this.debug = platform.config.debug
|
|
14
|
+
this.homeList = []
|
|
15
|
+
this.httpHost = platform.config.httpHost
|
|
16
|
+
this.ignoredDevices = platform.ignoredDevices
|
|
17
|
+
this.ignoredHomes = platform.config.ignoredHomes.split(',')
|
|
18
|
+
this.lang = platform.lang
|
|
19
|
+
this.log = platform.log
|
|
20
|
+
this.mode = platform.config.mode
|
|
21
|
+
this.obstructSwitches = platform.obstructSwitches
|
|
22
|
+
this.password = platform.config.password
|
|
23
|
+
this.sensorSwitches = Object
|
|
24
|
+
.values(platform.deviceConf)
|
|
25
|
+
.filter(el => el.sensorId && ['garage', 'lock'].includes(el.showAs))
|
|
26
|
+
.map(el => el.sensorId)
|
|
27
|
+
this.triedBase64 = false
|
|
28
|
+
this.username = platform.config.username
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async login() {
|
|
32
|
+
try {
|
|
33
|
+
// Used to log the user in and obtain the user api key and token
|
|
34
|
+
const data = {
|
|
35
|
+
countryCode: this.countryCode,
|
|
36
|
+
password: this.password,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// See if the user has provided an email or phone as username
|
|
40
|
+
if (this.username.includes('@')) {
|
|
41
|
+
data.email = this.username
|
|
42
|
+
} else {
|
|
43
|
+
data.phoneNumber = this.username
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Log the data depending on the debug setting
|
|
47
|
+
if (this.debug) {
|
|
48
|
+
this.log('%s.', this.lang.sendLogin)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Set up the request signature
|
|
52
|
+
const dataToSign = createHmac('sha256', this.appSecret)
|
|
53
|
+
.update(JSON.stringify(data))
|
|
54
|
+
.digest('base64')
|
|
55
|
+
|
|
56
|
+
// Send the request
|
|
57
|
+
const res = await axios.post(`https://${this.httpHost}/v2/user/login`, data, {
|
|
58
|
+
headers: {
|
|
59
|
+
'Authorization': `Sign ${dataToSign}`,
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
'X-CK-Appid': this.appId,
|
|
62
|
+
'X-CK-Nonce': Math.random()
|
|
63
|
+
.toString(36)
|
|
64
|
+
.substr(2, 8),
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// Parse the response
|
|
69
|
+
const body = res.data
|
|
70
|
+
if (body.error === 10004 && body?.data?.region) {
|
|
71
|
+
// In this case the user has been given a different region so retry login
|
|
72
|
+
const givenRegion = body.data.region
|
|
73
|
+
|
|
74
|
+
// Check the new received region is valid
|
|
75
|
+
switch (givenRegion) {
|
|
76
|
+
case 'eu':
|
|
77
|
+
case 'us':
|
|
78
|
+
case 'as':
|
|
79
|
+
this.httpHost = `${givenRegion}-apia.coolkit.cc`
|
|
80
|
+
break
|
|
81
|
+
case 'cn':
|
|
82
|
+
this.httpHost = 'cn-apia.coolkit.cn'
|
|
83
|
+
break
|
|
84
|
+
default:
|
|
85
|
+
throw new Error(`${this.lang.noRegionRec} - [${givenRegion}].`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Log the new http host if appropriate
|
|
89
|
+
if (this.debug) {
|
|
90
|
+
this.log('%s [%s].', this.lang.newRegionRec, this.httpHost)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Retry the login with the new http host
|
|
94
|
+
return await this.login()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if ([10001, 10014].includes(body.error) && !this.triedBase64) {
|
|
98
|
+
// In this case the password is incorrect so try base64 decoding just once
|
|
99
|
+
this.triedBase64 = true
|
|
100
|
+
this.password = Buffer.from(this.password, 'base64')
|
|
101
|
+
.toString('utf8')
|
|
102
|
+
.replace(/\r\n|\n|\r/g, '')
|
|
103
|
+
.trim()
|
|
104
|
+
return await this.login()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (body.data.at) {
|
|
108
|
+
// User api key and token received successfully
|
|
109
|
+
this.aToken = body.data.at
|
|
110
|
+
this.apiKey = body.data.user.apikey
|
|
111
|
+
return {
|
|
112
|
+
aToken: this.aToken,
|
|
113
|
+
apiKey: this.apiKey,
|
|
114
|
+
httpHost: this.httpHost,
|
|
115
|
+
password: this.password,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (body.error === 500) {
|
|
119
|
+
// Retry if another attempt could be successful
|
|
120
|
+
this.log.warn('%s.', this.lang.eweError)
|
|
121
|
+
await sleep(30000)
|
|
122
|
+
return await this.login()
|
|
123
|
+
}
|
|
124
|
+
if (body.msg) {
|
|
125
|
+
throw new Error(body.msg + (body.error ? ` [${body.error}]` : ''))
|
|
126
|
+
} else {
|
|
127
|
+
throw new Error(`${this.lang.noAuthRec}.\n${JSON.stringify(body, null, 2)}`)
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
// Check to see if it's a eWeLink server problem, and we can retry
|
|
131
|
+
if (err.code && platformConsts.httpRetryCodes.includes(err.code)) {
|
|
132
|
+
// Retry if another attempt could be successful
|
|
133
|
+
this.log.warn('%s [login() - %s].', this.lang.httpRetry, err.code)
|
|
134
|
+
await sleep(30000)
|
|
135
|
+
return this.login()
|
|
136
|
+
}
|
|
137
|
+
// It's not a eWeLink problem so report the error back
|
|
138
|
+
this.log.warn('%s.', this.lang.errLogin)
|
|
139
|
+
if (err.message.includes('10003')) {
|
|
140
|
+
this.log.warn('%s', this.lang.httpLogin10003)
|
|
141
|
+
}
|
|
142
|
+
throw err
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async getHomes() {
|
|
147
|
+
// Used to get a user's home list
|
|
148
|
+
try {
|
|
149
|
+
// Send the request
|
|
150
|
+
const res = await axios.get(`https://${this.httpHost}/v2/family`, {
|
|
151
|
+
headers: {
|
|
152
|
+
'Authorization': `Bearer ${this.aToken}`,
|
|
153
|
+
'Content-Type': 'application/json',
|
|
154
|
+
'X-CK-Appid': this.appId,
|
|
155
|
+
'X-CK-Nonce': Math.random()
|
|
156
|
+
.toString(36)
|
|
157
|
+
.substr(2, 8),
|
|
158
|
+
},
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// Parse the response
|
|
162
|
+
const body = res.data
|
|
163
|
+
if (
|
|
164
|
+
!body.data
|
|
165
|
+
|| body.error !== 0
|
|
166
|
+
|| !body.data.familyList
|
|
167
|
+
|| !Array.isArray(body.data.familyList)
|
|
168
|
+
) {
|
|
169
|
+
throw new Error(JSON.stringify(body, null, 2))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Add the home id to the global array of ids
|
|
173
|
+
body.data.familyList.forEach((home) => {
|
|
174
|
+
if (this.ignoredHomes.includes(home.id)) {
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
this.log('%s [%s] [%s].', this.lang.fetchHome, home.name, home.id)
|
|
178
|
+
this.homeList.push(home.id)
|
|
179
|
+
})
|
|
180
|
+
} catch (err) {
|
|
181
|
+
// Check to see if it's a eWeLink server problem, and we can retry
|
|
182
|
+
if (err.code && platformConsts.httpRetryCodes.includes(err.code)) {
|
|
183
|
+
// Retry if another attempt could be successful
|
|
184
|
+
this.log.warn('%s [getHomes() - %s].', this.lang.httpRetry, err.code)
|
|
185
|
+
await sleep(30000)
|
|
186
|
+
await this.getHomes()
|
|
187
|
+
} else {
|
|
188
|
+
// It's not a eWeLink problem so report the error back
|
|
189
|
+
this.log.warn('%s.', this.lang.errGetHomes)
|
|
190
|
+
throw err
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async getDevices() {
|
|
196
|
+
// Used to get a user's device list
|
|
197
|
+
try {
|
|
198
|
+
// Send the request to get a device list for each of the homes
|
|
199
|
+
const fullDeviceList = []
|
|
200
|
+
|
|
201
|
+
for (const homeId of this.homeList) {
|
|
202
|
+
const res = await axios.get(`https://${this.httpHost}/v2/device/thing`, {
|
|
203
|
+
headers: {
|
|
204
|
+
'Authorization': `Bearer ${this.aToken}`,
|
|
205
|
+
'Content-Type': 'application/json',
|
|
206
|
+
'X-CK-Appid': this.appId,
|
|
207
|
+
'X-CK-Nonce': Math.random()
|
|
208
|
+
.toString(36)
|
|
209
|
+
.substr(2, 8),
|
|
210
|
+
},
|
|
211
|
+
params: {
|
|
212
|
+
num: 0,
|
|
213
|
+
familyid: homeId,
|
|
214
|
+
},
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// Parse the response
|
|
218
|
+
const body = res.data
|
|
219
|
+
if (!body.data || body.error !== 0) {
|
|
220
|
+
throw new Error(JSON.stringify(body, null, 2))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// The list also includes scenes, so we need to remove them
|
|
224
|
+
if (body.data?.thingList.length > 0) {
|
|
225
|
+
body.data.thingList.forEach(device => fullDeviceList.push(device))
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Now we have a device list from all the eWeLink user homes
|
|
230
|
+
const deviceList = []
|
|
231
|
+
const sensorList = []
|
|
232
|
+
const groupList = []
|
|
233
|
+
fullDeviceList.forEach((d) => {
|
|
234
|
+
// Check each item is a device and also remove any devices the user has ignored
|
|
235
|
+
if (d?.itemData?.extra?.uiid && !this.ignoredDevices.includes(d.itemData.deviceid)) {
|
|
236
|
+
// If in LAN mode then don't add to device list
|
|
237
|
+
if (this.mode === 'lan' && !platformConsts.devices.lan.includes(d.itemData.extra.uiid)) {
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
// Separate the sensors as these need to be set up last
|
|
241
|
+
const isObstructSwitch = this.obstructSwitches[d.itemData.deviceid]
|
|
242
|
+
const isSensorSwitch = this.sensorSwitches[d.itemData.deviceid]
|
|
243
|
+
if (
|
|
244
|
+
platformConsts.devices.garageSensors.includes(d.itemData.extra.uiid)
|
|
245
|
+
|| isObstructSwitch
|
|
246
|
+
|| isSensorSwitch
|
|
247
|
+
) {
|
|
248
|
+
sensorList.push(d.itemData)
|
|
249
|
+
} else {
|
|
250
|
+
deviceList.push(d.itemData)
|
|
251
|
+
}
|
|
252
|
+
} else if (d.itemType === 3) {
|
|
253
|
+
// Is a group
|
|
254
|
+
groupList.push(d.itemData)
|
|
255
|
+
}
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
// Sensors need to go last as they update garages that need to exist already
|
|
259
|
+
return {
|
|
260
|
+
httpDeviceList: deviceList.concat(sensorList),
|
|
261
|
+
httpGroupList: groupList,
|
|
262
|
+
}
|
|
263
|
+
} catch (err) {
|
|
264
|
+
// Check to see if it's a eWeLink server problem, and we can retry
|
|
265
|
+
if (err.code && platformConsts.httpRetryCodes.includes(err.code)) {
|
|
266
|
+
// Retry if another attempt could be successful
|
|
267
|
+
this.log.warn('%s [getDevices() - %s].', this.lang.httpRetry, err.code)
|
|
268
|
+
await sleep(30000)
|
|
269
|
+
return this.getDevices()
|
|
270
|
+
}
|
|
271
|
+
// It's not a eWeLink problem so report the error back
|
|
272
|
+
this.log.warn('%s.', this.lang.errGetDevices)
|
|
273
|
+
throw err
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async updateGroup(groupId, params) {
|
|
278
|
+
// Used to get info about a specific device
|
|
279
|
+
const res = await axios.post(
|
|
280
|
+
`https://${this.httpHost}/v2/device/thing/status`,
|
|
281
|
+
{
|
|
282
|
+
type: 2,
|
|
283
|
+
id: groupId,
|
|
284
|
+
params,
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
headers: {
|
|
288
|
+
'Authorization': `Bearer ${this.aToken}`,
|
|
289
|
+
'Content-Type': 'application/json',
|
|
290
|
+
'X-CK-Appid': this.appId,
|
|
291
|
+
'X-CK-Nonce': Math.random()
|
|
292
|
+
.toString(36)
|
|
293
|
+
.substr(2, 8),
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
// Parse the response
|
|
299
|
+
const body = res.data
|
|
300
|
+
if (!body.data || body.error !== 0) {
|
|
301
|
+
throw new Error(JSON.stringify(body, null, 2))
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer'
|
|
2
|
+
import {
|
|
3
|
+
createCipheriv,
|
|
4
|
+
createDecipheriv,
|
|
5
|
+
createHash,
|
|
6
|
+
randomBytes,
|
|
7
|
+
} from 'node:crypto'
|
|
8
|
+
import events from 'node:events'
|
|
9
|
+
import axios from 'axios'
|
|
10
|
+
import dnsSd from '../node-dns-sd/lib/dns-sd.js'
|
|
11
|
+
import platformConsts from '../utils/constants.js'
|
|
12
|
+
import { hasProperty, parseError } from '../utils/functions.js'
|
|
13
|
+
|
|
14
|
+
const emitter = new events()
|
|
15
|
+
|
|
16
|
+
export default class {
|
|
17
|
+
constructor(platform) {
|
|
18
|
+
// Set up variables from the platform
|
|
19
|
+
this.debug = platform.config.debug
|
|
20
|
+
this.ipOverride = platform.ipOverride
|
|
21
|
+
this.lang = platform.lang
|
|
22
|
+
this.log = platform.log
|
|
23
|
+
this.mode = platform.config.mode
|
|
24
|
+
|
|
25
|
+
// Set up other variables and libraries needed by this class
|
|
26
|
+
this.deviceMap = new Map()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async getHosts() {
|
|
30
|
+
// Create a template map with our device list from the HTTP client
|
|
31
|
+
Object.entries(this.ipOverride).forEach((entry) => {
|
|
32
|
+
const [deviceId, ip] = entry
|
|
33
|
+
// Add this device into the map with its user configured IP
|
|
34
|
+
this.deviceMap.set(deviceId, {
|
|
35
|
+
ip,
|
|
36
|
+
ipOverride: true,
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Perform an initial discovery of devices on the local network
|
|
41
|
+
if (this.debug) {
|
|
42
|
+
this.log('%s.', this.lang.lanStarting)
|
|
43
|
+
}
|
|
44
|
+
const res = await dnsSd.discover({ name: '_ewelink._tcp.local' })
|
|
45
|
+
if (this.debug) {
|
|
46
|
+
this.log('%s.', this.lang.lanStarted)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Update the device map for each device found on the local network
|
|
50
|
+
res.forEach((device) => {
|
|
51
|
+
// Obtain the ewelink deviceId and check to see it is in our map
|
|
52
|
+
const deviceId = device.fqdn.replace('._ewelink._tcp.local', '').replace('eWeLink_', '')
|
|
53
|
+
|
|
54
|
+
// Do not override any overridden IPs
|
|
55
|
+
if (!this.deviceMap.has(deviceId)) {
|
|
56
|
+
this.deviceMap.set(deviceId, {
|
|
57
|
+
ip: device.address,
|
|
58
|
+
ipOverride: false,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
return this.deviceMap
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async startMonitor() {
|
|
66
|
+
// Function to parse DNS packets picked up by node-dns-sd
|
|
67
|
+
dnsSd.ondata = (packet) => {
|
|
68
|
+
try {
|
|
69
|
+
if (!packet.answers) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
packet.answers
|
|
73
|
+
.filter(value => value.name.includes('_ewelink._tcp.local'))
|
|
74
|
+
.filter(value => value.type === 'TXT')
|
|
75
|
+
.filter(value => this.deviceMap.has(value.rdata.id))
|
|
76
|
+
.filter(value => this.deviceMap.get(value.rdata.id).lanKey)
|
|
77
|
+
.forEach((value) => {
|
|
78
|
+
const { rdata } = value
|
|
79
|
+
|
|
80
|
+
// Check the packet relates to a device in our map
|
|
81
|
+
const deviceInfo = this.deviceMap.get(rdata.id)
|
|
82
|
+
|
|
83
|
+
// Skip the update if it's a duplicate
|
|
84
|
+
if (deviceInfo.lastIV === rdata.iv) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
deviceInfo.lastIV = rdata.iv
|
|
88
|
+
|
|
89
|
+
// Obtain the packet information
|
|
90
|
+
const data = rdata.data1 + (rdata.data2 || '') + (rdata.data3 || '') + (rdata.data4 || '')
|
|
91
|
+
const key = createHash('md5')
|
|
92
|
+
.update(Buffer.from(deviceInfo.lanKey, 'utf8'))
|
|
93
|
+
.digest()
|
|
94
|
+
const dText = createDecipheriv(
|
|
95
|
+
'aes-128-cbc',
|
|
96
|
+
key,
|
|
97
|
+
Buffer.from(rdata.iv, 'base64'),
|
|
98
|
+
)
|
|
99
|
+
const pText = Buffer.concat([
|
|
100
|
+
dText.update(Buffer.from(data, 'base64')),
|
|
101
|
+
dText.final(),
|
|
102
|
+
]).toString('utf8')
|
|
103
|
+
|
|
104
|
+
// Check to see if the IP address of the device has changed
|
|
105
|
+
if (packet.address !== deviceInfo.ip) {
|
|
106
|
+
deviceInfo.ip = deviceInfo.ipOverride ? deviceInfo.ip : packet.address
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.deviceMap.set(rdata.id, deviceInfo)
|
|
110
|
+
|
|
111
|
+
// Parse the deconstructed information from the packet
|
|
112
|
+
let params
|
|
113
|
+
try {
|
|
114
|
+
params = JSON.parse(pText)
|
|
115
|
+
} catch (err) {
|
|
116
|
+
this.log.warn(
|
|
117
|
+
'[%s] %s %s:\n%s',
|
|
118
|
+
rdata.id,
|
|
119
|
+
this.lang.cantReadPacket,
|
|
120
|
+
err.message,
|
|
121
|
+
pText,
|
|
122
|
+
)
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Remove any params we don't need
|
|
127
|
+
Object.keys(params).forEach((param) => {
|
|
128
|
+
if (!platformConsts.paramsToKeep.includes(param.replace(/\d/g, ''))) {
|
|
129
|
+
delete params[param]
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// If any params are left then generate the template to be emitted
|
|
134
|
+
if (Object.keys(params).length > 0) {
|
|
135
|
+
if (packet.address !== deviceInfo.ip) {
|
|
136
|
+
params.ip = packet.address
|
|
137
|
+
}
|
|
138
|
+
params.online = true
|
|
139
|
+
params.updateSource = 'LAN'
|
|
140
|
+
const returnTemplate = {
|
|
141
|
+
deviceid: rdata.id,
|
|
142
|
+
params,
|
|
143
|
+
}
|
|
144
|
+
emitter.emit('update', returnTemplate)
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
} catch (err) {
|
|
148
|
+
this.log.warn('%s %s.', this.lang.cantParsePacket, parseError(err))
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Start the DNS packet monitoring
|
|
153
|
+
await dnsSd.startMonitoring()
|
|
154
|
+
this.log('%s.', this.lang.lanMonitor)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async sendUpdate(json) {
|
|
158
|
+
try {
|
|
159
|
+
// Check this device exists in the map
|
|
160
|
+
if (!this.deviceMap.has(json.deviceid)) {
|
|
161
|
+
throw new Error(this.lang.devNotReachLAN)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const deviceInfo = this.deviceMap.get(json.deviceid)
|
|
165
|
+
|
|
166
|
+
// Check we have an IP address for the device
|
|
167
|
+
if (!deviceInfo.ip) {
|
|
168
|
+
throw new Error(this.lang.devNotReachLAN)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check we have the device lan key
|
|
172
|
+
if (!deviceInfo.lanKey || !deviceInfo.uiid || !deviceInfo.productModel) {
|
|
173
|
+
throw new Error(this.lang.devNoAPIKey)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const params = {}
|
|
177
|
+
let suffix
|
|
178
|
+
|
|
179
|
+
// Check the params to see which suffix and params we need
|
|
180
|
+
if (json.params.brightness) {
|
|
181
|
+
params.switch = 'on'
|
|
182
|
+
params.brightness = json.params.brightness
|
|
183
|
+
params.mode = 0
|
|
184
|
+
suffix = 'dimmable'
|
|
185
|
+
} else if (json.params.switches) {
|
|
186
|
+
const specialModels = ['iFan03', 'iFan', 'iFan04']
|
|
187
|
+
if (deviceInfo.uiid === 34 && specialModels.includes(deviceInfo.productModel)) {
|
|
188
|
+
// Special format for some iFan models
|
|
189
|
+
if (json.params.switches[0].outlet === 0) {
|
|
190
|
+
// Turn on/off the light for the iFan
|
|
191
|
+
params.light = json.params.switches[0].switch
|
|
192
|
+
suffix = 'light'
|
|
193
|
+
} else {
|
|
194
|
+
// Change the state or speed of the iFan
|
|
195
|
+
if (json.params.switches[0].switch === 'off') {
|
|
196
|
+
params.fan = 'off'
|
|
197
|
+
} else {
|
|
198
|
+
params.fan = 'on'
|
|
199
|
+
switch (json.params.switches[1].switch + json.params.switches[2].switch) {
|
|
200
|
+
case 'offoff':
|
|
201
|
+
params.speed = 1
|
|
202
|
+
break
|
|
203
|
+
case 'onoff':
|
|
204
|
+
params.speed = 2
|
|
205
|
+
break
|
|
206
|
+
case 'offon':
|
|
207
|
+
params.speed = 3
|
|
208
|
+
break
|
|
209
|
+
default:
|
|
210
|
+
// Should never happen
|
|
211
|
+
return 'ok'
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
suffix = 'fan'
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
params.switches = json.params.switches
|
|
218
|
+
suffix = 'switches'
|
|
219
|
+
}
|
|
220
|
+
} else if (json.params.switch) {
|
|
221
|
+
params.switch = json.params.switch
|
|
222
|
+
if (deviceInfo.uiid === 15) {
|
|
223
|
+
// TH10/16 only supports LAN in normal mode
|
|
224
|
+
if (json.params.deviceType === 'normal') {
|
|
225
|
+
// Extra params for the TH10/16
|
|
226
|
+
params.mainSwitch = json.params.mainSwitch
|
|
227
|
+
params.deviceType = json.params.deviceType
|
|
228
|
+
} else {
|
|
229
|
+
throw new Error(this.lang.devNotConfLAN)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
suffix = 'switch'
|
|
233
|
+
} else if (
|
|
234
|
+
hasProperty(json.params, 'location')
|
|
235
|
+
&& platformConsts.devices.switchMultiPower.includes(deviceInfo.uiid)
|
|
236
|
+
) {
|
|
237
|
+
// Use hasProperty has location can be INT 0
|
|
238
|
+
params.location = json.params.location
|
|
239
|
+
suffix = 'location'
|
|
240
|
+
} else if (json.params.uiActive && deviceInfo.uiid === 32) {
|
|
241
|
+
params.uiActive = json.params.uiActive
|
|
242
|
+
suffix = 'monitor'
|
|
243
|
+
} else if (json.params.cmd) {
|
|
244
|
+
params.cmd = 'transmit'
|
|
245
|
+
params.rfChl = json.params.rfChl
|
|
246
|
+
suffix = 'transmit'
|
|
247
|
+
} else if (hasProperty(json.params, 'ltype')) {
|
|
248
|
+
params.ltype = json.params.ltype
|
|
249
|
+
if (hasProperty(json.params, 'white')) {
|
|
250
|
+
params.white = json.params.white
|
|
251
|
+
} else {
|
|
252
|
+
params.color = json.params.color
|
|
253
|
+
}
|
|
254
|
+
suffix = 'dimmable'
|
|
255
|
+
} else {
|
|
256
|
+
throw new Error(this.lang.devNotConfLAN)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Generate the HTTP request
|
|
260
|
+
const key = createHash('md5')
|
|
261
|
+
.update(Buffer.from(deviceInfo.lanKey, 'utf8'))
|
|
262
|
+
.digest()
|
|
263
|
+
const iv = randomBytes(16)
|
|
264
|
+
const enc = createCipheriv('aes-128-cbc', key, iv)
|
|
265
|
+
const data = {
|
|
266
|
+
data: Buffer.concat([enc.update(JSON.stringify(params)), enc.final()]).toString('base64'),
|
|
267
|
+
deviceid: json.deviceid,
|
|
268
|
+
encrypt: true,
|
|
269
|
+
iv: iv.toString('base64'),
|
|
270
|
+
selfApikey: '123',
|
|
271
|
+
sequence: Date.now().toString(),
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Send the HTTP request
|
|
275
|
+
const res = await axios({
|
|
276
|
+
method: 'post',
|
|
277
|
+
url: `http://${deviceInfo.ip}:8081/zeroconf/${suffix}`,
|
|
278
|
+
headers: {
|
|
279
|
+
'Accept': 'application/json',
|
|
280
|
+
'Content-Type': 'application/json',
|
|
281
|
+
},
|
|
282
|
+
data,
|
|
283
|
+
timeout: this.mode === 'lan' ? 9000 : 3000,
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
// Check for any errors in the response
|
|
287
|
+
if (!res.data || res.data.error !== 0) {
|
|
288
|
+
const error = res?.data?.error || this.lang.lanErr
|
|
289
|
+
throw new Error(error)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// This ok is needed by the plugin
|
|
293
|
+
return 'ok'
|
|
294
|
+
} catch (err) {
|
|
295
|
+
return parseError(err)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
receiveUpdate(f) {
|
|
300
|
+
emitter.addListener('update', f)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
addDeviceDetailsToMap(deviceId, context) {
|
|
304
|
+
const entry = this.deviceMap.get(deviceId) || { ip: this.ipOverride[deviceId] }
|
|
305
|
+
entry.lanKey = context.lanKey
|
|
306
|
+
entry.uiid = context.eweUIID
|
|
307
|
+
entry.productModel = context.eweModel
|
|
308
|
+
this.deviceMap.set(deviceId, entry)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async closeConnection() {
|
|
312
|
+
// This is called when Homebridge is shutdown
|
|
313
|
+
await dnsSd.stopMonitoring()
|
|
314
|
+
if (this.debug) {
|
|
315
|
+
this.log('%s.', this.lang.stoppedLAN)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|