@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,414 @@
1
+ import events from 'node:events'
2
+ import axios from 'axios'
3
+ import wsp from 'websocket-as-promised'
4
+ import ws from 'ws'
5
+ import platformConsts from '../utils/constants.js'
6
+ import { hasProperty, parseError, sleep } from '../utils/functions.js'
7
+
8
+ const emitter = new events()
9
+
10
+ export default class {
11
+ constructor(platform, authData) {
12
+ // Set up variables from the platform
13
+ this.appId = platform.config.appId || platformConsts.appId
14
+ this.appSecret = platform.config.appSecret || platformConsts.appSecret
15
+ this.debug = platform.config.debug
16
+ this.lang = platform.lang
17
+ this.log = platform.log
18
+
19
+ // Set up variables from the authData (from HTTP)
20
+ this.httpHost = authData.httpHost
21
+ this.aToken = authData.aToken
22
+ this.apiKey = authData.apiKey
23
+
24
+ // Flag used to determine ws connection status
25
+ this.wsIsConnected = false
26
+ }
27
+
28
+ async getHost() {
29
+ // Used to get the web socket host
30
+ try {
31
+ // Send the HTTP request to get the web socket host
32
+ const res = await axios({
33
+ method: 'post',
34
+ url: `https://${this.httpHost.replace('-api', '-disp')}/dispatch/app`,
35
+ headers: {
36
+ 'Authorization': `Bearer ${this.aToken}`,
37
+ 'Content-Type': 'application/json',
38
+ },
39
+ data: {
40
+ appid: this.appId,
41
+ nonce: Math.random()
42
+ .toString(36)
43
+ .substr(2, 8),
44
+ ts: Math.floor(new Date().getTime() / 1000),
45
+ version: 8,
46
+ },
47
+ timeout: 6000,
48
+ })
49
+
50
+ // Parse the response
51
+ const body = res.data
52
+
53
+ // Check for any reason a host wasn't received
54
+ if (!body.domain) {
55
+ throw new Error(this.lang.noWSHost)
56
+ }
57
+
58
+ // Log the received host if appropriate
59
+ if (this.debug) {
60
+ this.log('%s [%s].', this.lang.wsHostRec, body.domain)
61
+ }
62
+
63
+ // Return the received web socket host
64
+ return body.domain
65
+ } catch (err) {
66
+ // Check to see if it's a eWeLink server problem, and we can retry
67
+ if (err.code && platformConsts.httpRetryCodes.includes(err.code)) {
68
+ // Retry if another attempt could be successful
69
+ this.log.warn('%s [getHost() - %s].', this.lang.httpRetry, err.code)
70
+ await sleep(30000)
71
+ return this.getHost()
72
+ }
73
+ // It's not a eWeLink problem so report the error back
74
+ this.log.warn('%s.', this.lang.errGetHost)
75
+ throw err
76
+ }
77
+ }
78
+
79
+ async login() {
80
+ // Used to create the web socket connection and authenticate
81
+ try {
82
+ // This may be called onClose and onError, we just want it to run once
83
+ if (this.debounce) {
84
+ return
85
+ }
86
+ this.debounce = true
87
+ setTimeout(() => {
88
+ this.debounce = false
89
+ }, 4000)
90
+
91
+ // Close any existing web socket connection
92
+ await this.closeConnection()
93
+
94
+ // Refresh the web socket host
95
+ const wsHost = await this.getHost()
96
+
97
+ // Create the web socket client
98
+ this.wsClient = new wsp(`wss://${wsHost}:8080/api/ws`, {
99
+ createWebSocket: url => new ws(url),
100
+ extractMessageData: event => event,
101
+ attachRequestId: (data, requestId) => ({ sequence: requestId, ...data }),
102
+ extractRequestId: data => data && data.sequence,
103
+ packMessage: data => JSON.stringify(data),
104
+ unpackMessage: (data) => {
105
+ data = data.toString()
106
+ return data === 'pong' ? data : JSON.parse(data)
107
+ },
108
+ })
109
+
110
+ // Add a listener to authenticate when a web socket connection is opened
111
+ this.wsClient.onOpen.addListener(async () => {
112
+ const sequence = Math.floor(new Date()).toString()
113
+
114
+ // Generate the login payload for the web socket
115
+ const payload = {
116
+ action: 'userOnline',
117
+ apikey: this.apiKey,
118
+ appid: this.appId,
119
+ at: this.aToken,
120
+ nonce: Math.random()
121
+ .toString(36)
122
+ .substr(2, 8),
123
+ sequence,
124
+ ts: Math.floor(new Date() / 1000),
125
+ userAgent: 'app',
126
+ version: 8,
127
+ }
128
+
129
+ // Log the web socket login if appropriate
130
+ if (this.debug) {
131
+ this.log('%s.', this.lang.wsLogin)
132
+ }
133
+
134
+ // Attempt to authenticate the web socket connection
135
+ try {
136
+ // Send the request
137
+ const res = await this.wsClient.sendRequest(payload, {
138
+ requestId: sequence,
139
+ })
140
+
141
+ // Parse the response
142
+ if (res.config && res.config.hb && res.config.hbInterval) {
143
+ // Update the flags
144
+ this.wsIsConnected = true
145
+
146
+ // Create a new ping interval
147
+ this.hbInterval = setInterval(() => {
148
+ try {
149
+ // Send the ping
150
+ this.wsClient.send('ping')
151
+ } catch (err) {
152
+ // Catch errors sending ping and show in debug mode
153
+ if (this.debug) {
154
+ this.log.warn('%s %s.', this.lang.wsPingError, parseError(err))
155
+ }
156
+ }
157
+ }, (res.config.hbInterval + 7) * 1000)
158
+
159
+ // Login was successful and log if appropriate
160
+ if (this.debug) {
161
+ this.log('%s.', this.lang.wsLoginSuccess)
162
+ }
163
+ } else if (res.error === 406) {
164
+ // There was a problem with the authentication response
165
+ this.log.warn(this.lang.wsLogin406)
166
+ } else {
167
+ // There was a problem with the authentication response
168
+ const str = JSON.stringify(res, null, 2)
169
+ throw new Error(`${this.lang.wsLoginErr}\n${str}`)
170
+ }
171
+ } catch (err) {
172
+ // Catch any errors authenticating the WS connection
173
+ this.log.warn('%s %s.', this.lang.wsLoginError, parseError(err))
174
+ }
175
+ })
176
+
177
+ // Add a listener for when we receive a WS message
178
+ this.wsClient.onUnpackedMessage.addListener((msg) => {
179
+ // Don't continue if it's a simple pong
180
+ if (msg === 'pong') {
181
+ return
182
+ }
183
+
184
+ // Set a device online flag now which we can change later
185
+ let onlineStatus = true
186
+
187
+ // Set up a params object if one didn't already come within the message
188
+ if (!msg.params) {
189
+ msg.params = {}
190
+ }
191
+
192
+ // We received a device on/offline or error notification
193
+ if (msg.deviceid && hasProperty(msg, 'error')) {
194
+ msg.action = 'update'
195
+ // Set the online status of the device
196
+ onlineStatus = msg.error === 0
197
+ }
198
+
199
+ // Normally the WS messages comes with an action
200
+ if (msg.action) {
201
+ // Check which action the WS message includes
202
+ switch (msg.action) {
203
+ case 'update':
204
+ case 'sysmsg': {
205
+ if (msg.action === 'sysmsg' && hasProperty(msg.params, 'online')) {
206
+ // Update the online/offline status provided in the message
207
+ onlineStatus = msg.params.online
208
+ }
209
+
210
+ // Loop through the device parameters received
211
+ Object.keys(msg.params).forEach((param) => {
212
+ // Remove any params that the plugin doesn't need
213
+ if (!platformConsts.paramsToKeep.includes(param.replace(/\d/g, ''))) {
214
+ delete msg.params[param]
215
+ }
216
+ })
217
+
218
+ if (Object.keys(msg.params).length > 0) {
219
+ // Add more params to report back to the plugin
220
+ msg.params.online = onlineStatus
221
+ msg.params.updateSource = 'WS'
222
+
223
+ // Generate the object to return to the plugin
224
+ const returnTemplate = {
225
+ deviceid: msg.deviceid,
226
+ params: msg.params,
227
+ }
228
+
229
+ // Report the new device params object back to the plugin to deal with
230
+ emitter.emit('update', returnTemplate)
231
+ }
232
+ break
233
+ }
234
+ case 'reportSubDevice':
235
+ case 'subDevice':
236
+ // We don't need to do anything with this action
237
+ return
238
+ default: {
239
+ this.log.warn(
240
+ '[%s] %s.\n%s',
241
+ msg.deviceid,
242
+ this.lang.wsUnkAct,
243
+ JSON.stringify(msg, null, 2),
244
+ )
245
+ }
246
+ }
247
+ } else if (hasProperty(msg, 'error') && msg.error === 0) {
248
+ // Safe to ignore these messages
249
+ } else {
250
+ // WS message received has an unknown action
251
+ this.log.warn('%s.\n%s', this.lang.wsUnkCmd, JSON.stringify(msg, null, 2))
252
+ }
253
+ })
254
+
255
+ // Add a listener for when the web socket closes for any reason
256
+ this.wsClient.onClose.addListener(async (e) => {
257
+ // Don't continue further with logging/reconnection if it's a wanted closure
258
+ if (e === 1005) {
259
+ return
260
+ }
261
+
262
+ // Log that the web socket has closed if appropriate
263
+ if (this.debug) {
264
+ this.log.warn('%s [%s].', this.lang.wsReconnectError, e)
265
+ }
266
+
267
+ // Wait for 4 seconds and try to re-establish the web socket connection
268
+ await sleep(4000)
269
+ await this.login()
270
+ })
271
+
272
+ // Add a listener for when the web socket throws an error
273
+ this.wsClient.onError.addListener(async (e) => {
274
+ // Log that the web socket has closed if appropriate
275
+ if (this.debug) {
276
+ this.log.warn('%s [%s].', this.lang.wsReconnectClose, e)
277
+ }
278
+
279
+ // Wait for 6 seconds and try to re-establish the web socket connection
280
+ await sleep(6000)
281
+ await this.login()
282
+ })
283
+
284
+ // Open the web socket connection
285
+ await this.wsClient.open()
286
+ } catch (err) {
287
+ const errToShow = parseError(err)
288
+
289
+ // Log the error if appropriate (debug on)
290
+ if (this.debug) {
291
+ this.log('%s: %s.', this.lang.wsLoginErrRecon, errToShow)
292
+ }
293
+
294
+ // Wait for 5 seconds and try to re-establish the web socket connection
295
+ await sleep(5000)
296
+ await this.login()
297
+ }
298
+ }
299
+
300
+ async sendUpdate(json) {
301
+ // Generate the payload to send
302
+ const toSend = {
303
+ ...json,
304
+ ...{
305
+ action: 'update',
306
+ ts: 0,
307
+ userAgent: 'app',
308
+ },
309
+ }
310
+
311
+ // Check the web socket is open
312
+ if (this.wsClient && this.wsIsConnected) {
313
+ try {
314
+ // Generate the sequence that will be attached to the message
315
+ const sequence = Math.floor(new Date()).toString()
316
+
317
+ // Send the request to eWeLink
318
+ const res = await this.wsClient.sendRequest(toSend, {
319
+ requestId: sequence,
320
+ })
321
+
322
+ // Parse the response and see if any error has been reported back
323
+ res.error = hasProperty(res, 'error') ? res.error : -1
324
+
325
+ // Check if and which error has been reported back
326
+ switch (res.error) {
327
+ case 0:
328
+ // No error is always what is wanted
329
+ if (res.config && res.config.hundredDaysKwhData) {
330
+ return res.config.hundredDaysKwhData
331
+ }
332
+ return true
333
+ case 504:
334
+ throw new Error(`${this.lang.wsReqTimeout} [504]`)
335
+ default:
336
+ // An error has occurred
337
+ throw new Error(`${this.lang.wsUnkRes} [${res.error}]`)
338
+ }
339
+ } catch (err) {
340
+ // Catch any errors sending the device update
341
+ throw new Error(parseError(err))
342
+ }
343
+ } else {
344
+ // The web socket isn't currently open so log this
345
+ throw new Error(this.lang.wsResend)
346
+ }
347
+ }
348
+
349
+ receiveUpdate(f) {
350
+ emitter.addListener('update', f)
351
+ }
352
+
353
+ async requestUpdate(accessory) {
354
+ // Don't continue if disabled for any eWeLink UIID
355
+ if (platformConsts.devices.skipUpdateRequest.includes(accessory.context.eweUIID)) {
356
+ return
357
+ }
358
+
359
+ // Log the action if appropriate
360
+ if (accessory.control.enableDebugLogging) {
361
+ this.log('[%s] %s.', accessory.displayName, this.lang.updReq)
362
+ }
363
+
364
+ // Generate the payload to send
365
+ const json = {
366
+ action: 'query',
367
+ apikey: accessory.context.eweApiKey,
368
+ deviceid: accessory.context.eweDeviceId,
369
+ params: [],
370
+ sequence: Math.floor(new Date()).toString(),
371
+ ts: 0,
372
+ userAgent: 'app',
373
+ }
374
+
375
+ // Check the web socket is open
376
+ if (this.wsClient && this.wsIsConnected) {
377
+ // Send the request
378
+ this.wsClient.send(JSON.stringify(json))
379
+ } else {
380
+ // The web socket isn't currently open so log this
381
+ throw new Error(this.lang.wsResend)
382
+ }
383
+ }
384
+
385
+ async closeConnection() {
386
+ // This is called when refreshing connection or if Homebridge shuts down
387
+
388
+ // Clear any existing ping interval
389
+ if (this.hbInterval) {
390
+ clearInterval(this.hbInterval)
391
+ this.hbInterval = null
392
+ }
393
+
394
+ // Check the ws client is set up
395
+ if (this.wsClient) {
396
+ // Remove any existing listeners
397
+ this.wsClient.removeAllListeners()
398
+
399
+ // Close the web socket connection
400
+ if (this.wsIsConnected) {
401
+ // Set the connection flag to false
402
+ this.wsIsConnected = false
403
+
404
+ // Close the connection
405
+ await this.wsClient.close()
406
+
407
+ // Log if appropriate
408
+ if (this.debug) {
409
+ this.log('%s.', this.lang.stoppedWS)
410
+ }
411
+ }
412
+ }
413
+ }
414
+ }