@homebridge-plugins/homebridge-ewelink 12.11.1-beta.0 → 12.11.2-beta.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 +49 -1
- package/lib/connection/http.js +27 -13
- package/lib/connection/lan.js +24 -3
- package/lib/connection/ws.js +62 -27
- package/lib/device/curtain.js +18 -1
- package/lib/device/fan.js +1 -1
- package/lib/device/group.js +1 -1
- package/lib/device/light-dimmer.js +5 -0
- package/lib/device/light-rgb-cct.js +10 -4
- package/lib/device/motor.js +27 -6
- package/lib/device/outlet-multi.js +38 -9
- package/lib/device/outlet-single-inched.js +11 -7
- package/lib/device/outlet-single.js +21 -8
- package/lib/device/panel.js +1 -1
- package/lib/device/rf-button.js +3 -0
- package/lib/device/sensor-ambient.js +20 -6
- package/lib/device/sensor-temp-humi.js +16 -3
- package/lib/device/simulation/blind.js +4 -3
- package/lib/device/simulation/cooler.js +6 -6
- package/lib/device/simulation/door.js +4 -3
- package/lib/device/simulation/doorbell.js +4 -3
- package/lib/device/simulation/garage-one.js +5 -4
- package/lib/device/simulation/garage-two.js +5 -4
- package/lib/device/simulation/gate-one.js +4 -3
- package/lib/device/simulation/heater.js +6 -6
- package/lib/device/simulation/lock-one.js +4 -3
- package/lib/device/simulation/p-button.js +4 -3
- package/lib/device/simulation/purifier.js +4 -3
- package/lib/device/simulation/sensor.js +4 -3
- package/lib/device/simulation/switch-valve.js +5 -4
- package/lib/device/simulation/tap-one.js +4 -3
- package/lib/device/simulation/tap-two.js +4 -3
- package/lib/device/simulation/th-cooler.js +4 -3
- package/lib/device/simulation/th-dehumidifier.js +4 -3
- package/lib/device/simulation/th-heater.js +4 -3
- package/lib/device/simulation/th-humidifier.js +4 -3
- package/lib/device/simulation/th-thermostat.js +4 -3
- package/lib/device/simulation/tv.js +4 -3
- package/lib/device/simulation/valve-one.js +4 -3
- package/lib/device/simulation/valve-two.js +4 -3
- package/lib/device/simulation/window.js +4 -3
- package/lib/device/switch-man.js +10 -1
- package/lib/device/switch-mate.js +8 -0
- package/lib/device/switch-multi.js +39 -9
- package/lib/device/switch-single-inched.js +7 -3
- package/lib/device/switch-single.js +23 -6
- package/lib/device/zigbee/sensor-ambient.js +10 -2
- package/lib/device/zigbee/sensor-motion.js +9 -0
- package/lib/device/zigbee/switch-stateless.js +8 -0
- package/lib/device/zigbee/thermostat.js +4 -4
- package/lib/device/zigbee/zigbee-water-valve.js +5 -0
- package/lib/platform.js +70 -44
- package/lib/utils/constants.js +22 -6
- package/lib/utils/functions.js +3 -6
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@homebridge-plugins/homebridge-ewelink` will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## v12.11.
|
|
5
|
+
## v12.11.2 (Pending Release)
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- fix: replace `Math.random()` nonce generation with `crypto.randomBytes()`
|
|
10
|
+
- fix: filter glitched sensor readings
|
|
11
|
+
- fix: LAN packet parse errors
|
|
12
|
+
- fix: ws 406 auth re-login with backoff
|
|
13
|
+
- fix: ws dispatch fallback hosts
|
|
14
|
+
- fix: ws command timeout and retry
|
|
15
|
+
- fix: LAN retry on `ECONNRESET`
|
|
16
|
+
- perf: LAN commands bypass queue
|
|
17
|
+
- feat: add new device UIIDs
|
|
18
|
+
- fix: POWR3 `operSide` parameter
|
|
19
|
+
- fix: dimmer brightness requires switch on
|
|
20
|
+
- feat: curtain mid-movement stop
|
|
21
|
+
- feat: `offlineAsOff` for switches and outlets
|
|
22
|
+
- fix: RF button `ConfiguredName` display
|
|
23
|
+
- fix: LAN `operSide` passthrough
|
|
24
|
+
- fix: zigbee multi-channel send all channels
|
|
25
|
+
- fix: button event deduplication via `trigTime`
|
|
26
|
+
- fix: ws minimum interval between messages
|
|
27
|
+
- fix: MINI-DIM brightness params
|
|
28
|
+
- fix: ws unmatched response device updates
|
|
29
|
+
- fix: skip stale zigbee `subDevRssi` updates
|
|
30
|
+
- fix: zigbee motion reset on bridge offline
|
|
31
|
+
- fix: TRVZB firmware type coercion
|
|
32
|
+
- fix: UIID 104 `ltype` undefined in params
|
|
33
|
+
- fix: ws reconnection exponential backoff
|
|
34
|
+
- perf: in-memory temperature cache for simulations
|
|
35
|
+
- feat: LAN discovery summary logging
|
|
36
|
+
- fix: cover position boundary clamping
|
|
37
|
+
- fix: HTTP 401 automatic token refresh
|
|
38
|
+
- fix: clean stale services on category change
|
|
39
|
+
- feat: `sledonline` LAN power monitoring trick
|
|
40
|
+
- feat: T5 (UIID 211) cover motor mode
|
|
41
|
+
- fix: clear command queue on shutdown
|
|
42
|
+
- fix: switches array bounds checking
|
|
43
|
+
- fix: guard `accessory.control` access
|
|
44
|
+
- fix: async forEach to for...of loop
|
|
45
|
+
- fix: `getHost()` unbounded recursion limit
|
|
46
|
+
- fix: cap energy readings array size
|
|
47
|
+
- refactor: replace per-device shutdown listeners with `destroy()`
|
|
48
|
+
- fix: revert UIID 52 from `lightCCT`
|
|
49
|
+
- fix: simulation switches array bounds
|
|
50
|
+
- fix: audit fixes from code review
|
|
51
|
+
- chore: dependency updates
|
|
52
|
+
|
|
53
|
+
## v12.11.1 (2026-04-05)
|
|
6
54
|
|
|
7
55
|
### Changed
|
|
8
56
|
|
package/lib/connection/http.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer'
|
|
2
|
-
import { createHmac } from 'node:crypto'
|
|
2
|
+
import { createHmac, randomBytes } from 'node:crypto'
|
|
3
3
|
|
|
4
4
|
import axios from 'axios'
|
|
5
5
|
|
|
@@ -28,6 +28,26 @@ export default class {
|
|
|
28
28
|
.map(el => el.sensorId)
|
|
29
29
|
this.triedBase64 = false
|
|
30
30
|
this.username = platform.config.username
|
|
31
|
+
|
|
32
|
+
// Set up axios interceptor for automatic 401 token refresh
|
|
33
|
+
this.axiosClient = axios.create()
|
|
34
|
+
this.axiosClient.interceptors.response.use(
|
|
35
|
+
res => res,
|
|
36
|
+
async (err) => {
|
|
37
|
+
if (err.response?.status === 401 && !err.config._retried) {
|
|
38
|
+
err.config._retried = true
|
|
39
|
+
try {
|
|
40
|
+
const authData = await this.login()
|
|
41
|
+
this.aToken = authData.aToken
|
|
42
|
+
err.config.headers.Authorization = `Bearer ${this.aToken}`
|
|
43
|
+
return this.axiosClient(err.config)
|
|
44
|
+
} catch (loginErr) {
|
|
45
|
+
throw err
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw err
|
|
49
|
+
},
|
|
50
|
+
)
|
|
31
51
|
}
|
|
32
52
|
|
|
33
53
|
async login() {
|
|
@@ -61,9 +81,7 @@ export default class {
|
|
|
61
81
|
'Authorization': `Sign ${dataToSign}`,
|
|
62
82
|
'Content-Type': 'application/json',
|
|
63
83
|
'X-CK-Appid': this.appId,
|
|
64
|
-
'X-CK-Nonce':
|
|
65
|
-
.toString(36)
|
|
66
|
-
.substring(2, 10),
|
|
84
|
+
'X-CK-Nonce': randomBytes(4).toString('hex'),
|
|
67
85
|
},
|
|
68
86
|
})
|
|
69
87
|
|
|
@@ -149,14 +167,12 @@ export default class {
|
|
|
149
167
|
// Used to get a user's home list
|
|
150
168
|
try {
|
|
151
169
|
// Send the request
|
|
152
|
-
const res = await
|
|
170
|
+
const res = await this.axiosClient.get(`https://${this.httpHost}/v2/family`, {
|
|
153
171
|
headers: {
|
|
154
172
|
'Authorization': `Bearer ${this.aToken}`,
|
|
155
173
|
'Content-Type': 'application/json',
|
|
156
174
|
'X-CK-Appid': this.appId,
|
|
157
|
-
'X-CK-Nonce':
|
|
158
|
-
.toString(36)
|
|
159
|
-
.substring(2, 10),
|
|
175
|
+
'X-CK-Nonce': randomBytes(4).toString('hex'),
|
|
160
176
|
},
|
|
161
177
|
})
|
|
162
178
|
|
|
@@ -201,7 +217,7 @@ export default class {
|
|
|
201
217
|
const fullDeviceList = []
|
|
202
218
|
|
|
203
219
|
for (const homeId of this.homeList) {
|
|
204
|
-
const res = await
|
|
220
|
+
const res = await this.axiosClient.get(`https://${this.httpHost}/v2/device/thing`, {
|
|
205
221
|
headers: {
|
|
206
222
|
'Authorization': `Bearer ${this.aToken}`,
|
|
207
223
|
'Content-Type': 'application/json',
|
|
@@ -278,7 +294,7 @@ export default class {
|
|
|
278
294
|
|
|
279
295
|
async updateGroup(groupId, params) {
|
|
280
296
|
// Used to get info about a specific device
|
|
281
|
-
const res = await
|
|
297
|
+
const res = await this.axiosClient.post(
|
|
282
298
|
`https://${this.httpHost}/v2/device/thing/status`,
|
|
283
299
|
{
|
|
284
300
|
type: 2,
|
|
@@ -290,9 +306,7 @@ export default class {
|
|
|
290
306
|
'Authorization': `Bearer ${this.aToken}`,
|
|
291
307
|
'Content-Type': 'application/json',
|
|
292
308
|
'X-CK-Appid': this.appId,
|
|
293
|
-
'X-CK-Nonce':
|
|
294
|
-
.toString(36)
|
|
295
|
-
.substring(2, 10),
|
|
309
|
+
'X-CK-Nonce': randomBytes(4).toString('hex'),
|
|
296
310
|
},
|
|
297
311
|
},
|
|
298
312
|
)
|
package/lib/connection/lan.js
CHANGED
|
@@ -11,7 +11,7 @@ import axios from 'axios'
|
|
|
11
11
|
|
|
12
12
|
import dnsSd from '../node-dns-sd/lib/dns-sd.js'
|
|
13
13
|
import platformConsts from '../utils/constants.js'
|
|
14
|
-
import { hasProperty, parseError } from '../utils/functions.js'
|
|
14
|
+
import { hasProperty, parseError, sleep } from '../utils/functions.js'
|
|
15
15
|
|
|
16
16
|
const emitter = new events()
|
|
17
17
|
|
|
@@ -113,7 +113,10 @@ export default class {
|
|
|
113
113
|
// Parse the deconstructed information from the packet
|
|
114
114
|
let params
|
|
115
115
|
try {
|
|
116
|
-
|
|
116
|
+
// Fix RF Bridge malformed JSON and strip trailing garbage bytes
|
|
117
|
+
// eslint-disable-next-line no-control-regex
|
|
118
|
+
const cleanText = pText.replace(/"\s*=\s*"/g, '":"').replace(/[\x00-\x1F]+$/g, '')
|
|
119
|
+
params = JSON.parse(cleanText)
|
|
117
120
|
} catch (err) {
|
|
118
121
|
this.log.warn(
|
|
119
122
|
'[%s] %s %s:\n%s',
|
|
@@ -154,9 +157,16 @@ export default class {
|
|
|
154
157
|
// Start the DNS packet monitoring
|
|
155
158
|
await dnsSd.startMonitoring()
|
|
156
159
|
this.log('%s.', this.lang.lanMonitor)
|
|
160
|
+
|
|
161
|
+
// Log LAN discovery summary after 10 seconds
|
|
162
|
+
setTimeout(() => {
|
|
163
|
+
const total = this.deviceMap.size
|
|
164
|
+
const withIp = [...this.deviceMap.values()].filter(d => d.ip).length
|
|
165
|
+
this.log('LAN discovery: %s/%s devices found locally.', withIp, total)
|
|
166
|
+
}, 10000)
|
|
157
167
|
}
|
|
158
168
|
|
|
159
|
-
async sendUpdate(json) {
|
|
169
|
+
async sendUpdate(json, retries = 0) {
|
|
160
170
|
try {
|
|
161
171
|
// Check this device exists in the map
|
|
162
172
|
if (!this.deviceMap.has(json.deviceid)) {
|
|
@@ -217,6 +227,9 @@ export default class {
|
|
|
217
227
|
}
|
|
218
228
|
} else {
|
|
219
229
|
params.switches = json.params.switches
|
|
230
|
+
if (json.params.operSide !== undefined) {
|
|
231
|
+
params.operSide = json.params.operSide
|
|
232
|
+
}
|
|
220
233
|
suffix = 'switches'
|
|
221
234
|
}
|
|
222
235
|
} else if (json.params.switch) {
|
|
@@ -239,6 +252,9 @@ export default class {
|
|
|
239
252
|
// Use hasProperty has location can be INT 0
|
|
240
253
|
params.location = json.params.location
|
|
241
254
|
suffix = 'location'
|
|
255
|
+
} else if (hasProperty(json.params, 'sledOnline')) {
|
|
256
|
+
params.sledOnline = json.params.sledOnline ? 'on' : 'off'
|
|
257
|
+
suffix = 'switch'
|
|
242
258
|
} else if (json.params.uiActive && deviceInfo.uiid === 32) {
|
|
243
259
|
params.uiActive = json.params.uiActive
|
|
244
260
|
suffix = 'monitor'
|
|
@@ -297,6 +313,11 @@ export default class {
|
|
|
297
313
|
// This ok is needed by the plugin
|
|
298
314
|
return 'ok'
|
|
299
315
|
} catch (err) {
|
|
316
|
+
// Retry on ECONNRESET (device web server is single-threaded)
|
|
317
|
+
if (err.code === 'ECONNRESET' && retries < 10) {
|
|
318
|
+
await sleep(100)
|
|
319
|
+
return this.sendUpdate(json, retries + 1)
|
|
320
|
+
}
|
|
300
321
|
return parseError(err)
|
|
301
322
|
}
|
|
302
323
|
}
|
package/lib/connection/ws.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto'
|
|
1
2
|
import events from 'node:events'
|
|
2
3
|
|
|
3
4
|
import axios from 'axios'
|
|
@@ -27,7 +28,7 @@ export default class {
|
|
|
27
28
|
this.wsIsConnected = false
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
async getHost() {
|
|
31
|
+
async getHost(attempt = 0) {
|
|
31
32
|
// Used to get the web socket host
|
|
32
33
|
try {
|
|
33
34
|
// Send the HTTP request to get the web socket host
|
|
@@ -40,9 +41,7 @@ export default class {
|
|
|
40
41
|
},
|
|
41
42
|
data: {
|
|
42
43
|
appid: this.appId,
|
|
43
|
-
nonce:
|
|
44
|
-
.toString(36)
|
|
45
|
-
.substring(2, 10),
|
|
44
|
+
nonce: randomBytes(4).toString('hex'),
|
|
46
45
|
ts: Math.floor(Date.now() / 1000),
|
|
47
46
|
version: 8,
|
|
48
47
|
},
|
|
@@ -66,15 +65,18 @@ export default class {
|
|
|
66
65
|
return body.domain
|
|
67
66
|
} catch (err) {
|
|
68
67
|
// Check to see if it's a eWeLink server problem, and we can retry
|
|
69
|
-
if (err.code && platformConsts.httpRetryCodes.includes(err.code)) {
|
|
70
|
-
// Retry if another attempt could be successful
|
|
68
|
+
if (err.code && platformConsts.httpRetryCodes.includes(err.code) && attempt < 3) {
|
|
71
69
|
this.log.warn('%s [getHost() - %s].', this.lang.httpRetry, err.code)
|
|
72
70
|
await sleep(30000)
|
|
73
|
-
return this.getHost()
|
|
71
|
+
return this.getHost(attempt + 1)
|
|
74
72
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
|
|
74
|
+
// Fall back to static pconnect hosts
|
|
75
|
+
const region = this.httpHost.split('-')[0]
|
|
76
|
+
const suffix = region === 'cn' ? 'coolkit.cn' : 'coolkit.cc'
|
|
77
|
+
const fallback = `${region}-pconnect3.${suffix}`
|
|
78
|
+
this.log.warn('%s [%s].', this.lang.errGetHost, fallback)
|
|
79
|
+
return fallback
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
|
|
@@ -107,6 +109,7 @@ export default class {
|
|
|
107
109
|
data = data.toString()
|
|
108
110
|
return data === 'pong' ? data : JSON.parse(data)
|
|
109
111
|
},
|
|
112
|
+
timeout: 20000,
|
|
110
113
|
})
|
|
111
114
|
|
|
112
115
|
// Add a listener to authenticate when a web socket connection is opened
|
|
@@ -119,9 +122,7 @@ export default class {
|
|
|
119
122
|
apikey: this.apiKey,
|
|
120
123
|
appid: this.appId,
|
|
121
124
|
at: this.aToken,
|
|
122
|
-
nonce:
|
|
123
|
-
.toString(36)
|
|
124
|
-
.substring(2, 10),
|
|
125
|
+
nonce: randomBytes(4).toString('hex'),
|
|
125
126
|
sequence,
|
|
126
127
|
ts: Math.floor(new Date() / 1000),
|
|
127
128
|
userAgent: 'app',
|
|
@@ -159,12 +160,19 @@ export default class {
|
|
|
159
160
|
}, (res.config.hbInterval + 7) * 1000)
|
|
160
161
|
|
|
161
162
|
// Login was successful and log if appropriate
|
|
163
|
+
this.authRetries = 0
|
|
162
164
|
if (this.debug) {
|
|
163
165
|
this.log('%s.', this.lang.wsLoginSuccess)
|
|
164
166
|
}
|
|
165
167
|
} else if (res.error === 406) {
|
|
166
|
-
//
|
|
168
|
+
// Token invalidated (e.g. concurrent session), re-authenticate with backoff
|
|
167
169
|
this.log.warn(this.lang.wsLogin406)
|
|
170
|
+
const delay = Math.min(5000 * 2 ** (this.authRetries || 0), 300000)
|
|
171
|
+
this.authRetries = (this.authRetries || 0) + 1
|
|
172
|
+
if (this.authRetries <= 10) {
|
|
173
|
+
await sleep(delay)
|
|
174
|
+
await this.login()
|
|
175
|
+
}
|
|
168
176
|
} else {
|
|
169
177
|
// There was a problem with the authentication response
|
|
170
178
|
const str = JSON.stringify(res, null, 2)
|
|
@@ -209,6 +217,12 @@ export default class {
|
|
|
209
217
|
onlineStatus = msg.params.online
|
|
210
218
|
}
|
|
211
219
|
|
|
220
|
+
// Skip updates that only contain subDevRssi (stale Zigbee data)
|
|
221
|
+
const paramKeys = Object.keys(msg.params).filter(k => k !== 'online')
|
|
222
|
+
if (paramKeys.length === 1 && paramKeys[0] === 'subDevRssi') {
|
|
223
|
+
break
|
|
224
|
+
}
|
|
225
|
+
|
|
212
226
|
// Loop through the device parameters received
|
|
213
227
|
Object.keys(msg.params).forEach((param) => {
|
|
214
228
|
// Remove any params that the plugin doesn't need
|
|
@@ -247,7 +261,12 @@ export default class {
|
|
|
247
261
|
}
|
|
248
262
|
}
|
|
249
263
|
} else if (hasProperty(msg, 'error') && msg.error === 0) {
|
|
250
|
-
//
|
|
264
|
+
// Process device params from unmatched responses (e.g. query results)
|
|
265
|
+
if (msg.deviceid && msg.params) {
|
|
266
|
+
msg.params.online = true
|
|
267
|
+
msg.params.updateSource = 'WS'
|
|
268
|
+
emitter.emit('update', { deviceid: msg.deviceid, params: msg.params })
|
|
269
|
+
}
|
|
251
270
|
} else {
|
|
252
271
|
// WS message received has an unknown action
|
|
253
272
|
this.log.warn('%s.\n%s', this.lang.wsUnkCmd, JSON.stringify(msg, null, 2))
|
|
@@ -261,45 +280,49 @@ export default class {
|
|
|
261
280
|
return
|
|
262
281
|
}
|
|
263
282
|
|
|
264
|
-
// Log that the web socket has closed if appropriate
|
|
265
283
|
if (this.debug) {
|
|
266
284
|
this.log.warn('%s [%s].', this.lang.wsReconnectError, e)
|
|
267
285
|
}
|
|
268
286
|
|
|
269
|
-
//
|
|
270
|
-
|
|
287
|
+
// Reconnect with exponential backoff
|
|
288
|
+
this.reconnectAttempts = (this.reconnectAttempts || 0) + 1
|
|
289
|
+
const delay = Math.min(5000 * 2 ** (this.reconnectAttempts - 1), 300000)
|
|
290
|
+
await sleep(delay)
|
|
271
291
|
await this.login()
|
|
272
292
|
})
|
|
273
293
|
|
|
274
294
|
// Add a listener for when the web socket throws an error
|
|
275
295
|
this.wsClient.onError.addListener(async (e) => {
|
|
276
|
-
// Log that the web socket has closed if appropriate
|
|
277
296
|
if (this.debug) {
|
|
278
297
|
this.log.warn('%s [%s].', this.lang.wsReconnectClose, e)
|
|
279
298
|
}
|
|
280
299
|
|
|
281
|
-
|
|
282
|
-
|
|
300
|
+
this.reconnectAttempts = (this.reconnectAttempts || 0) + 1
|
|
301
|
+
const delay = Math.min(5000 * 2 ** (this.reconnectAttempts - 1), 300000)
|
|
302
|
+
await sleep(delay)
|
|
283
303
|
await this.login()
|
|
284
304
|
})
|
|
285
305
|
|
|
286
306
|
// Open the web socket connection
|
|
287
307
|
await this.wsClient.open()
|
|
308
|
+
|
|
309
|
+
// Reset reconnect counter on successful connection
|
|
310
|
+
this.reconnectAttempts = 0
|
|
288
311
|
} catch (err) {
|
|
289
312
|
const errToShow = parseError(err)
|
|
290
313
|
|
|
291
|
-
// Log the error if appropriate (debug on)
|
|
292
314
|
if (this.debug) {
|
|
293
315
|
this.log('%s: %s.', this.lang.wsLoginErrRecon, errToShow)
|
|
294
316
|
}
|
|
295
317
|
|
|
296
|
-
|
|
297
|
-
|
|
318
|
+
this.reconnectAttempts = (this.reconnectAttempts || 0) + 1
|
|
319
|
+
const delay = Math.min(5000 * 2 ** (this.reconnectAttempts - 1), 300000)
|
|
320
|
+
await sleep(delay)
|
|
298
321
|
await this.login()
|
|
299
322
|
}
|
|
300
323
|
}
|
|
301
324
|
|
|
302
|
-
async sendUpdate(json) {
|
|
325
|
+
async sendUpdate(json, retries = 0) {
|
|
303
326
|
// Generate the payload to send
|
|
304
327
|
const toSend = {
|
|
305
328
|
...json,
|
|
@@ -309,6 +332,13 @@ export default class {
|
|
|
309
332
|
userAgent: 'app',
|
|
310
333
|
}
|
|
311
334
|
|
|
335
|
+
// Enforce minimum 100ms between cloud messages
|
|
336
|
+
const now = Date.now()
|
|
337
|
+
if (this.lastSendTime && now - this.lastSendTime < 100) {
|
|
338
|
+
await sleep(100 - (now - this.lastSendTime))
|
|
339
|
+
}
|
|
340
|
+
this.lastSendTime = Date.now()
|
|
341
|
+
|
|
312
342
|
// Check the web socket is open
|
|
313
343
|
if (this.wsClient && this.wsIsConnected) {
|
|
314
344
|
try {
|
|
@@ -338,8 +368,13 @@ export default class {
|
|
|
338
368
|
throw new Error(`${this.lang.wsUnkRes} [${res.error}]`)
|
|
339
369
|
}
|
|
340
370
|
} catch (err) {
|
|
341
|
-
//
|
|
342
|
-
|
|
371
|
+
// Retry up to 3 times on timeout errors only
|
|
372
|
+
const errMsg = parseError(err)
|
|
373
|
+
if (retries < 3 && (errMsg.includes('504') || errMsg.includes('timeout') || errMsg.includes('Timeout'))) {
|
|
374
|
+
await sleep(1000)
|
|
375
|
+
return this.sendUpdate(json, retries + 1)
|
|
376
|
+
}
|
|
377
|
+
throw new Error(errMsg)
|
|
343
378
|
}
|
|
344
379
|
} else {
|
|
345
380
|
// The web socket isn't currently open so log this
|
package/lib/device/curtain.js
CHANGED
|
@@ -170,6 +170,22 @@ export default class {
|
|
|
170
170
|
// Create the params object to send
|
|
171
171
|
const params = {}
|
|
172
172
|
|
|
173
|
+
// Mid-movement stop: if target is within 5% of current, send stop
|
|
174
|
+
if (Math.abs(value - this.cachePos) <= 5 && value !== 0 && value !== 100) {
|
|
175
|
+
switch (this.accessory.context.eweUIID) {
|
|
176
|
+
case 11:
|
|
177
|
+
params.switch = 'pause'
|
|
178
|
+
break
|
|
179
|
+
case 67:
|
|
180
|
+
params.op = 2
|
|
181
|
+
break
|
|
182
|
+
default:
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
await this.platform.sendDeviceUpdate(this.accessory, params)
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
173
189
|
switch (this.accessory.context.eweUIID) {
|
|
174
190
|
case 11:
|
|
175
191
|
// If we are to fully open/close the curtain we can use the switch param
|
|
@@ -299,7 +315,8 @@ export default class {
|
|
|
299
315
|
return
|
|
300
316
|
}
|
|
301
317
|
|
|
302
|
-
//
|
|
318
|
+
// Clamp and update HomeKit with the provided value
|
|
319
|
+
newPos = Math.max(Math.min(newPos, 100), 0)
|
|
303
320
|
this.service.updateCharacteristic(this.hapChar.TargetPosition, newPos)
|
|
304
321
|
this.service.updateCharacteristic(this.hapChar.CurrentPosition, newPos)
|
|
305
322
|
this.service.updateCharacteristic(this.hapChar.PositionState, 2)
|
package/lib/device/fan.js
CHANGED
package/lib/device/group.js
CHANGED
|
@@ -82,7 +82,7 @@ export default class {
|
|
|
82
82
|
this.log('[%s] %s [%s].', this.name, this.lang.curState, this.cacheState)
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
-
if (this.isSCM && params.switches && params.switches[0].switch !== this.cacheState) {
|
|
85
|
+
if (this.isSCM && params.switches?.[0] && params.switches[0].switch !== this.cacheState) {
|
|
86
86
|
this.cacheState = params.switches[0].switch
|
|
87
87
|
this.service.updateCharacteristic(this.hapChar.On, this.cacheState === 'on')
|
|
88
88
|
this.accessory.eveService.addEntry({ status: this.cacheState === 'on' ? 1 : 0 })
|
|
@@ -145,6 +145,11 @@ export default class {
|
|
|
145
145
|
params.brightness = value
|
|
146
146
|
// D1 eWeLink scale matches HomeKit scale of 0-100
|
|
147
147
|
params.mode = 0
|
|
148
|
+
params.switch = 'on'
|
|
149
|
+
break
|
|
150
|
+
case 277:
|
|
151
|
+
params.brightness = Math.max(value, 1)
|
|
152
|
+
params.switch = 'on'
|
|
148
153
|
break
|
|
149
154
|
case 57:
|
|
150
155
|
params.channel0 = Math.round((value * 23) / 10 + 25).toString()
|
|
@@ -343,10 +343,9 @@ export default class {
|
|
|
343
343
|
colorB: rgb[2],
|
|
344
344
|
}
|
|
345
345
|
break
|
|
346
|
-
case 104:
|
|
346
|
+
case 104: {
|
|
347
347
|
// B02-B-A60, B05-B-A60, GTLC104
|
|
348
348
|
params = {
|
|
349
|
-
ltype: this.cacheMode === 'color' ? undefined : 'color',
|
|
350
349
|
color: {
|
|
351
350
|
br: this.cacheBright,
|
|
352
351
|
r: rgb[0],
|
|
@@ -354,7 +353,11 @@ export default class {
|
|
|
354
353
|
b: rgb[2],
|
|
355
354
|
},
|
|
356
355
|
}
|
|
356
|
+
if (this.cacheMode !== 'color') {
|
|
357
|
+
params.ltype = 'color'
|
|
358
|
+
}
|
|
357
359
|
break
|
|
360
|
+
}
|
|
358
361
|
case 135:
|
|
359
362
|
case 136:
|
|
360
363
|
params = {
|
|
@@ -443,17 +446,20 @@ export default class {
|
|
|
443
446
|
}
|
|
444
447
|
break
|
|
445
448
|
}
|
|
446
|
-
case 104:
|
|
449
|
+
case 104: {
|
|
447
450
|
// B02-B-A60, B05-B-A60, GTLC104
|
|
448
451
|
scaledCT = Math.round(((mToK - 2700) / 3800) * 255)
|
|
449
452
|
params = {
|
|
450
|
-
ltype: this.cacheMode === 'white' ? undefined : 'white',
|
|
451
453
|
white: {
|
|
452
454
|
br: this.cacheBright,
|
|
453
455
|
ct: scaledCT,
|
|
454
456
|
},
|
|
455
457
|
}
|
|
458
|
+
if (this.cacheMode !== 'white') {
|
|
459
|
+
params.ltype = 'white'
|
|
460
|
+
}
|
|
456
461
|
break
|
|
462
|
+
}
|
|
457
463
|
case 135:
|
|
458
464
|
case 136:
|
|
459
465
|
scaledCT = Math.round(((mToK - 2700) / 3800) * 100)
|
package/lib/device/motor.js
CHANGED
|
@@ -165,9 +165,6 @@ export default class {
|
|
|
165
165
|
this.internalUIUpdate()
|
|
166
166
|
this.intervalPoll = setInterval(() => this.internalUIUpdate(), 120000)
|
|
167
167
|
}, 5000)
|
|
168
|
-
|
|
169
|
-
// Stop the intervals on Homebridge shutdown
|
|
170
|
-
platform.api.on('shutdown', () => clearInterval(this.intervalPoll))
|
|
171
168
|
}
|
|
172
169
|
|
|
173
170
|
// Output the customised options to the log
|
|
@@ -190,7 +187,9 @@ export default class {
|
|
|
190
187
|
if (updateKey !== this.updateKey) {
|
|
191
188
|
return
|
|
192
189
|
}
|
|
193
|
-
const params =
|
|
190
|
+
const params = this.accessory.context.eweUIID === 211
|
|
191
|
+
? { percentageControl: 100 - value }
|
|
192
|
+
: { location: value }
|
|
194
193
|
await this.platform.sendDeviceUpdate(this.accessory, params)
|
|
195
194
|
this.cacheTarg = value
|
|
196
195
|
if (this.enableLogging) {
|
|
@@ -211,7 +210,9 @@ export default class {
|
|
|
211
210
|
// 0 for open, 1 for closed
|
|
212
211
|
// this.cachePos is 0 for closed and >0 for open
|
|
213
212
|
|
|
214
|
-
const params =
|
|
213
|
+
const params = this.accessory.context.eweUIID === 211
|
|
214
|
+
? { electromotor: value === 1 ? 2 : 0 }
|
|
215
|
+
: { location: value === 1 ? 0 : 100 }
|
|
215
216
|
await this.platform.sendDeviceUpdate(this.accessory, params)
|
|
216
217
|
this.cacheTarg = value === 1 ? 0 : 100
|
|
217
218
|
if (this.enableLogging) {
|
|
@@ -245,11 +246,27 @@ export default class {
|
|
|
245
246
|
try {
|
|
246
247
|
let locationParams = false
|
|
247
248
|
|
|
249
|
+
// T5 cover uses percentageControl (inverted: 0=open, 100=closed)
|
|
250
|
+
if (hasProperty(params, 'percentageControl') && params.calibState) {
|
|
251
|
+
locationParams = true
|
|
252
|
+
const newPos = Math.max(Math.min(100 - params.percentageControl, 100), 0)
|
|
253
|
+
if (newPos !== this.cachePos) {
|
|
254
|
+
this.cachePos = newPos
|
|
255
|
+
if (!this.isGarage) {
|
|
256
|
+
this.service.updateCharacteristic(this.hapChar.CurrentPosition, this.cachePos)
|
|
257
|
+
this.service.updateCharacteristic(this.hapChar.TargetPosition, this.cachePos)
|
|
258
|
+
}
|
|
259
|
+
if (params.updateSource && this.enableLogging) {
|
|
260
|
+
this.log('[%s] %s [%s%].', this.name, this.lang.curPos, this.cachePos)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
248
265
|
// currLocation is the current position of the motor
|
|
249
266
|
if (hasProperty(params, 'currLocation')) {
|
|
250
267
|
locationParams = true
|
|
251
268
|
if (params.currLocation !== this.cachePos) {
|
|
252
|
-
this.cachePos = params.currLocation
|
|
269
|
+
this.cachePos = Math.max(Math.min(params.currLocation, 100), 0)
|
|
253
270
|
if (!this.isGarage) {
|
|
254
271
|
this.service.updateCharacteristic(this.hapChar.CurrentPosition, this.cachePos)
|
|
255
272
|
}
|
|
@@ -357,6 +374,10 @@ export default class {
|
|
|
357
374
|
}
|
|
358
375
|
}
|
|
359
376
|
|
|
377
|
+
destroy() {
|
|
378
|
+
clearInterval(this.intervalPoll)
|
|
379
|
+
}
|
|
380
|
+
|
|
360
381
|
markStatus(isOnline) {
|
|
361
382
|
this.isOnline = isOnline
|
|
362
383
|
}
|