@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.
Files changed (113) hide show
  1. package/CHANGELOG.md +1363 -0
  2. package/LICENSE +21 -0
  3. package/README.md +68 -0
  4. package/config.schema.json +1757 -0
  5. package/eslint.config.js +15 -0
  6. package/lib/connection/api.js +508 -0
  7. package/lib/connection/http.js +304 -0
  8. package/lib/connection/lan.js +318 -0
  9. package/lib/connection/ws.js +414 -0
  10. package/lib/device/curtain.js +324 -0
  11. package/lib/device/diffuser.js +473 -0
  12. package/lib/device/fan.js +349 -0
  13. package/lib/device/group.js +106 -0
  14. package/lib/device/humidifier.js +199 -0
  15. package/lib/device/index.js +61 -0
  16. package/lib/device/light-cct.js +333 -0
  17. package/lib/device/light-dimmer.js +257 -0
  18. package/lib/device/light-rgb-cct.js +809 -0
  19. package/lib/device/light-rgb.js +272 -0
  20. package/lib/device/motor.js +363 -0
  21. package/lib/device/outlet-multi.js +417 -0
  22. package/lib/device/outlet-single-inched.js +409 -0
  23. package/lib/device/outlet-single.js +384 -0
  24. package/lib/device/panel.js +382 -0
  25. package/lib/device/rf-bridge.js +321 -0
  26. package/lib/device/rf-button.js +125 -0
  27. package/lib/device/rf-sensor.js +173 -0
  28. package/lib/device/sensor-ambient.js +311 -0
  29. package/lib/device/sensor-contact.js +140 -0
  30. package/lib/device/sensor-temp-humi.js +182 -0
  31. package/lib/device/simulation/blind.js +253 -0
  32. package/lib/device/simulation/cooler.js +312 -0
  33. package/lib/device/simulation/door.js +253 -0
  34. package/lib/device/simulation/doorbell.js +190 -0
  35. package/lib/device/simulation/garage-eachen.js +235 -0
  36. package/lib/device/simulation/garage-four.js +176 -0
  37. package/lib/device/simulation/garage-od-switch.js +36 -0
  38. package/lib/device/simulation/garage-one.js +395 -0
  39. package/lib/device/simulation/garage-two.js +394 -0
  40. package/lib/device/simulation/gate-one.js +327 -0
  41. package/lib/device/simulation/heater.js +389 -0
  42. package/lib/device/simulation/index.js +75 -0
  43. package/lib/device/simulation/light-fan.js +258 -0
  44. package/lib/device/simulation/lock-eachen.js +163 -0
  45. package/lib/device/simulation/lock-one.js +366 -0
  46. package/lib/device/simulation/p-button.js +223 -0
  47. package/lib/device/simulation/purifier.js +289 -0
  48. package/lib/device/simulation/rf-blind.js +192 -0
  49. package/lib/device/simulation/rf-door.js +180 -0
  50. package/lib/device/simulation/rf-window.js +180 -0
  51. package/lib/device/simulation/sensor-hidden.js +105 -0
  52. package/lib/device/simulation/sensor-leak.js +123 -0
  53. package/lib/device/simulation/sensor-visible.js +231 -0
  54. package/lib/device/simulation/sensor.js +300 -0
  55. package/lib/device/simulation/switch-valve.js +351 -0
  56. package/lib/device/simulation/tap-one.js +283 -0
  57. package/lib/device/simulation/tap-two.js +266 -0
  58. package/lib/device/simulation/th-cooler.js +443 -0
  59. package/lib/device/simulation/th-dehumidifier.js +424 -0
  60. package/lib/device/simulation/th-heater.js +449 -0
  61. package/lib/device/simulation/th-humidifier.js +424 -0
  62. package/lib/device/simulation/th-thermostat.js +611 -0
  63. package/lib/device/simulation/tv.js +252 -0
  64. package/lib/device/simulation/valve-four.js +232 -0
  65. package/lib/device/simulation/valve-one.js +345 -0
  66. package/lib/device/simulation/valve-two.js +340 -0
  67. package/lib/device/simulation/window.js +253 -0
  68. package/lib/device/switch-man.js +106 -0
  69. package/lib/device/switch-mate.js +82 -0
  70. package/lib/device/switch-multi.js +411 -0
  71. package/lib/device/switch-single-inched.js +332 -0
  72. package/lib/device/switch-single.js +307 -0
  73. package/lib/device/template.js +43 -0
  74. package/lib/device/thermostat.js +286 -0
  75. package/lib/device/zigbee/index.js +27 -0
  76. package/lib/device/zigbee/light-cct.js +263 -0
  77. package/lib/device/zigbee/light-dimmer.js +148 -0
  78. package/lib/device/zigbee/light-rgb-cct.js +400 -0
  79. package/lib/device/zigbee/motor.js +301 -0
  80. package/lib/device/zigbee/sensor-ambient.js +177 -0
  81. package/lib/device/zigbee/sensor-contact.js +137 -0
  82. package/lib/device/zigbee/sensor-leak.js +119 -0
  83. package/lib/device/zigbee/sensor-motion.js +119 -0
  84. package/lib/device/zigbee/sensor-occupancy.js +119 -0
  85. package/lib/device/zigbee/sensor-smoke.js +96 -0
  86. package/lib/device/zigbee/switch-stateless.js +104 -0
  87. package/lib/device/zigbee/thermostat.js +323 -0
  88. package/lib/fakegato/LICENSE +21 -0
  89. package/lib/fakegato/fakegato-history.js +813 -0
  90. package/lib/fakegato/fakegato-storage.js +108 -0
  91. package/lib/fakegato/fakegato-timer.js +125 -0
  92. package/lib/fakegato/uuid.js +27 -0
  93. package/lib/homebridge-ui/public/index.html +365 -0
  94. package/lib/homebridge-ui/server.js +46 -0
  95. package/lib/index.js +7 -0
  96. package/lib/node-dns-sd/LICENSE +21 -0
  97. package/lib/node-dns-sd/README.md +613 -0
  98. package/lib/node-dns-sd/lib/dns-sd-classes.json +6 -0
  99. package/lib/node-dns-sd/lib/dns-sd-composer.js +83 -0
  100. package/lib/node-dns-sd/lib/dns-sd-parser.js +344 -0
  101. package/lib/node-dns-sd/lib/dns-sd-types.json +52 -0
  102. package/lib/node-dns-sd/lib/dns-sd.js +580 -0
  103. package/lib/platform.js +2156 -0
  104. package/lib/utils/colour.js +552 -0
  105. package/lib/utils/constants.js +589 -0
  106. package/lib/utils/custom-chars.js +33 -0
  107. package/lib/utils/eve-chars.js +130 -0
  108. package/lib/utils/functions.js +42 -0
  109. package/lib/utils/lang-en.js +160 -0
  110. package/lib/utils/lang-fr.js +163 -0
  111. package/lib/utils/lang-th.js +160 -0
  112. package/lib/utils/lang.js +5 -0
  113. 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
+ }