@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +49 -1
  2. package/lib/connection/http.js +27 -13
  3. package/lib/connection/lan.js +24 -3
  4. package/lib/connection/ws.js +62 -27
  5. package/lib/device/curtain.js +18 -1
  6. package/lib/device/fan.js +1 -1
  7. package/lib/device/group.js +1 -1
  8. package/lib/device/light-dimmer.js +5 -0
  9. package/lib/device/light-rgb-cct.js +10 -4
  10. package/lib/device/motor.js +27 -6
  11. package/lib/device/outlet-multi.js +38 -9
  12. package/lib/device/outlet-single-inched.js +11 -7
  13. package/lib/device/outlet-single.js +21 -8
  14. package/lib/device/panel.js +1 -1
  15. package/lib/device/rf-button.js +3 -0
  16. package/lib/device/sensor-ambient.js +20 -6
  17. package/lib/device/sensor-temp-humi.js +16 -3
  18. package/lib/device/simulation/blind.js +4 -3
  19. package/lib/device/simulation/cooler.js +6 -6
  20. package/lib/device/simulation/door.js +4 -3
  21. package/lib/device/simulation/doorbell.js +4 -3
  22. package/lib/device/simulation/garage-one.js +5 -4
  23. package/lib/device/simulation/garage-two.js +5 -4
  24. package/lib/device/simulation/gate-one.js +4 -3
  25. package/lib/device/simulation/heater.js +6 -6
  26. package/lib/device/simulation/lock-one.js +4 -3
  27. package/lib/device/simulation/p-button.js +4 -3
  28. package/lib/device/simulation/purifier.js +4 -3
  29. package/lib/device/simulation/sensor.js +4 -3
  30. package/lib/device/simulation/switch-valve.js +5 -4
  31. package/lib/device/simulation/tap-one.js +4 -3
  32. package/lib/device/simulation/tap-two.js +4 -3
  33. package/lib/device/simulation/th-cooler.js +4 -3
  34. package/lib/device/simulation/th-dehumidifier.js +4 -3
  35. package/lib/device/simulation/th-heater.js +4 -3
  36. package/lib/device/simulation/th-humidifier.js +4 -3
  37. package/lib/device/simulation/th-thermostat.js +4 -3
  38. package/lib/device/simulation/tv.js +4 -3
  39. package/lib/device/simulation/valve-one.js +4 -3
  40. package/lib/device/simulation/valve-two.js +4 -3
  41. package/lib/device/simulation/window.js +4 -3
  42. package/lib/device/switch-man.js +10 -1
  43. package/lib/device/switch-mate.js +8 -0
  44. package/lib/device/switch-multi.js +39 -9
  45. package/lib/device/switch-single-inched.js +7 -3
  46. package/lib/device/switch-single.js +23 -6
  47. package/lib/device/zigbee/sensor-ambient.js +10 -2
  48. package/lib/device/zigbee/sensor-motion.js +9 -0
  49. package/lib/device/zigbee/switch-stateless.js +8 -0
  50. package/lib/device/zigbee/thermostat.js +4 -4
  51. package/lib/device/zigbee/zigbee-water-valve.js +5 -0
  52. package/lib/platform.js +70 -44
  53. package/lib/utils/constants.js +22 -6
  54. package/lib/utils/functions.js +3 -6
  55. 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.1 (Pending Release)
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
 
@@ -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': Math.random()
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 axios.get(`https://${this.httpHost}/v2/family`, {
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': Math.random()
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 axios.get(`https://${this.httpHost}/v2/device/thing`, {
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 axios.post(
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': Math.random()
294
- .toString(36)
295
- .substring(2, 10),
309
+ 'X-CK-Nonce': randomBytes(4).toString('hex'),
296
310
  },
297
311
  },
298
312
  )
@@ -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
- params = JSON.parse(pText)
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
  }
@@ -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: Math.random()
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
- // It's not a eWeLink problem so report the error back
76
- this.log.warn('%s.', this.lang.errGetHost)
77
- throw err
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: Math.random()
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
- // There was a problem with the authentication response
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
- // Safe to ignore these messages
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
- // Wait for 4 seconds and try to re-establish the web socket connection
270
- await sleep(4000)
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
- // Wait for 6 seconds and try to re-establish the web socket connection
282
- await sleep(6000)
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
- // Wait for 5 seconds and try to re-establish the web socket connection
297
- await sleep(5000)
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
- // Catch any errors sending the device update
342
- throw new Error(parseError(err))
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
@@ -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
- // Update HomeKit with the provided value
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
@@ -270,7 +270,7 @@ export default class {
270
270
  return
271
271
  }
272
272
  }
273
- if (!params.switches) {
273
+ if (!params.switches || !params.switches[1] || !params.switches[2] || !params.switches[3]) {
274
274
  return
275
275
  }
276
276
  if (params.switches[1].switch !== this.cacheState) {
@@ -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)
@@ -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 = { location: value }
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 = { location: value === 1 ? 0 : 100 }
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
  }