@homebridge-plugins/homebridge-ewelink 12.19.1-beta.8 → 12.20.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  All notable changes to `@homebridge-plugins/homebridge-ewelink` will be documented in this file.
4
4
 
5
- ## v12.19.1 (Pending Release)
5
+ ## v12.20.0 (2026-11-14)
6
6
 
7
7
  ### Changed
8
8
 
@@ -14,7 +14,6 @@ All notable changes to `@homebridge-plugins/homebridge-ewelink` will be document
14
14
  - fix(zigbee): read the curtain motor's position, so home stops showing it opening (#810)
15
15
  - fix(zigbee): keep a curtain motor shown as a garage door in step with its door
16
16
  - fix(zigbee): flip the zbu-curt-7006 position, which counts 0 as open (#810)
17
- - chore(deps): dependency updates
18
17
  - fix: stop live updates dropping readings like light level, child lock and fan oscillation
19
18
  - fix: show power readings on the 226 and 7032 when they are shown as an outlet
20
19
  - fix: read inched power devices in hundredths, so a powr316 or s61 no longer shows 5500w for 55w
@@ -30,6 +29,28 @@ All notable changes to `@homebridge-plugins/homebridge-ewelink` will be document
30
29
  - fix(zigbee): show a curtain motor at rest after a restart, not stuck opening (#810)
31
30
  - fix: log the state each device starts from in debug mode, to trace a wrong start (#810)
32
31
  - fix(zigbee): ignore the stale position the cloud keeps for a zbu-curt-7006 (#810)
32
+ - fix(rf): set up a curtain shown as a blind, door or window, which failed to start
33
+ - fix: accept the zigbee multi channel model the settings offer, without a false warning
34
+ - fix(lan): send the channels a scene changes on one switch as one request (@mzsoliman)
35
+ - chore: leave CLAUDE.md out of the published package
36
+ - feat(zigbee): add the tp-wgzba wall thermostat, with heat and auto modes
37
+ - feat(zigbee): warn when a curtain motor's end positions were never set (#810)
38
+ - fix: say which server and login type the http login uses, in debug mode (#818)
39
+ - fix: show the sonoff sc's light level the right way up, as 1 is its brightest
40
+ - fix: grade the sonoff sc's dust level with the bands its protocol gives
41
+ - fix: stop a plug's total energy in eve growing 10 times too fast
42
+ - fix: keep a pale colour pale when a uiid 104 bulb or the diffuser reports it
43
+ - fix: switch a circuit breaker set as inched with true, as its protocol asks
44
+ - feat: add the uiid 294 two-way metering socket, with power readings
45
+ - feat: add the uiid 163 four-channel remote switch, like its one to three channel siblings
46
+ - fix: control a uiid 52 light with the state and channel values its protocol uses
47
+ - feat: show the three-speed humidifier's humidity and temperature, and warn when its tank is empty
48
+ - feat: dim the sonoff b1 and set its white tone, as its protocol allows in white mode
49
+ - feat: show the mini-1gsp's wall switch presses as a button when its relay is separated
50
+ - fix: turn all four channels of a four-channel power switch from its main tile
51
+ - feat: add the uiid 289 and 290 matter metering switches, with power for each channel
52
+ - ci: publish releases with node.js v26, the current lts
53
+ - chore(deps): dependency updates
33
54
 
34
55
  ## v12.19.0 (2026-10-18)
35
56
 
@@ -6,6 +6,19 @@ import axios from 'axios'
6
6
  import platformConsts from '../utils/constants.js'
7
7
  import { sleep } from '../utils/functions.js'
8
8
 
9
+ // Keep enough of a login to spot a typo (length, country code, leading digits)
10
+ // while hiding the rest, so a debug log can be shared publicly
11
+ export function maskUsername(username = '') {
12
+ if (username.includes('@')) {
13
+ const [name, domain] = username.split('@')
14
+ return `${name.slice(0, 1)}***@${domain}`
15
+ }
16
+ if (username.length <= 7) {
17
+ return '*'.repeat(Math.max(username.length - 2, 0)) + username.slice(-2)
18
+ }
19
+ return username.slice(0, 5) + '*'.repeat(username.length - 7) + username.slice(-2)
20
+ }
21
+
9
22
  export default class {
10
23
  constructor(platform) {
11
24
  // Set up variables from the platform
@@ -83,8 +96,15 @@ export default class {
83
96
  data.phoneNumber = this.username
84
97
  }
85
98
 
86
- // Log the data depending on the debug setting
87
- this.log.debug('%s.', this.lang.sendLogin)
99
+ // Say which server is used and what the login is sent as, without the details themselves (#818)
100
+ this.log.debug(
101
+ '%s to [%s] with %s [%s] and country code [%s].',
102
+ this.lang.sendLogin,
103
+ this.httpHost,
104
+ data.email ? 'email' : 'phone number',
105
+ maskUsername(this.username),
106
+ this.countryCode,
107
+ )
88
108
 
89
109
  // Set up the request signature
90
110
  const dataToSign = createHmac('sha256', this.appSecret)
@@ -131,6 +151,7 @@ export default class {
131
151
 
132
152
  if ([10001, 10014].includes(body.error) && !this.triedBase64) {
133
153
  // In this case the password is incorrect so try base64 decoding just once
154
+ this.log.debug('%s.', this.lang.loginBase64)
134
155
  this.triedBase64 = true
135
156
  this.password = Buffer.from(this.password, 'base64')
136
157
  .toString('utf8')
@@ -15,6 +15,28 @@ import { hasProperty, parseError, sleep } from '../utils/functions.js'
15
15
 
16
16
  const emitter = new events()
17
17
 
18
+ // How long a channel update waits for others to the same device before sending.
19
+ // A HomeKit scene writes every channel it touches in one go, so this is enough
20
+ // to catch them all while being too short for anyone to notice.
21
+ const MERGE_WINDOW_MS = 50
22
+
23
+ // Some iFan models put the light and fan speed in the switches array by position,
24
+ // so two of their updates cannot be combined outlet by outlet
25
+ const NO_MERGE_MODELS = ['iFan03', 'iFan', 'iFan04']
26
+
27
+ // Add channel updates into a pending list, a later update to the same outlet
28
+ // replacing the earlier one
29
+ function mergeSwitches(pending, incoming) {
30
+ incoming.forEach((update) => {
31
+ const index = pending.findIndex(p => p.outlet === update.outlet)
32
+ if (index === -1) {
33
+ pending.push({ ...update })
34
+ } else {
35
+ pending[index] = { ...update }
36
+ }
37
+ })
38
+ }
39
+
18
40
  export default class {
19
41
  constructor(platform) {
20
42
  // Set up variables from the platform
@@ -25,6 +47,12 @@ export default class {
25
47
 
26
48
  // Set up other variables and libraries needed by this class
27
49
  this.deviceMap = new Map()
50
+
51
+ // The last request queued for each device, so the next one waits for it
52
+ this.deviceQueues = new Map()
53
+
54
+ // Channel updates still collecting others before they are sent, per device
55
+ this.openBatches = new Map()
28
56
  }
29
57
 
30
58
  async getHosts() {
@@ -168,7 +196,64 @@ export default class {
168
196
  }, 10000)
169
197
  }
170
198
 
171
- async sendUpdate(json, retries = 0) {
199
+ async sendUpdate(json) {
200
+ // The device web server handles one request at a time, so a burst of them
201
+ // (a scene turning several channels of one switch) left the later ones waiting
202
+ // past the timeout. Requests to a device go one at a time, and channel updates
203
+ // that arrive together are merged into a single request.
204
+ if (!this.canMerge(json)) {
205
+ return this.queueForDevice(json.deviceid, () => this.sendRequest(json))
206
+ }
207
+
208
+ // Join a request that is still waiting to be sent, if there is one
209
+ const batch = this.openBatches.get(json.deviceid)
210
+ if (batch) {
211
+ mergeSwitches(batch.json.params.switches, json.params.switches)
212
+ return batch.result
213
+ }
214
+
215
+ const newBatch = {
216
+ json: {
217
+ ...json,
218
+ params: {
219
+ switches: [...json.params.switches],
220
+ },
221
+ },
222
+ }
223
+ this.openBatches.set(json.deviceid, newBatch)
224
+ newBatch.result = this.queueForDevice(json.deviceid, async () => {
225
+ await sleep(MERGE_WINDOW_MS)
226
+ this.openBatches.delete(json.deviceid)
227
+ return this.sendRequest(newBatch.json)
228
+ })
229
+ return newBatch.result
230
+ }
231
+
232
+ canMerge(json) {
233
+ const keys = Object.keys(json.params || {})
234
+ if (keys.length !== 1 || !Array.isArray(json.params.switches)) {
235
+ return false
236
+ }
237
+ const deviceInfo = this.deviceMap.get(json.deviceid)
238
+ return !(deviceInfo?.uiid === 34 && NO_MERGE_MODELS.includes(deviceInfo.productModel))
239
+ }
240
+
241
+ queueForDevice(deviceId, task) {
242
+ // sendRequest reports failures as a returned message rather than throwing,
243
+ // so the chain only needs to guard against something unexpected
244
+ const previous = this.deviceQueues.get(deviceId) || Promise.resolve()
245
+ const result = previous.then(task)
246
+ const tail = result.catch(() => {})
247
+ this.deviceQueues.set(deviceId, tail)
248
+ tail.then(() => {
249
+ if (this.deviceQueues.get(deviceId) === tail) {
250
+ this.deviceQueues.delete(deviceId)
251
+ }
252
+ })
253
+ return result
254
+ }
255
+
256
+ async sendRequest(json, retries = 0) {
172
257
  try {
173
258
  // Check this device exists in the map
174
259
  if (!this.deviceMap.has(json.deviceid)) {
@@ -323,7 +408,7 @@ export default class {
323
408
  // Retry on ECONNRESET (device web server is single-threaded)
324
409
  if (err.code === 'ECONNRESET' && retries < 10) {
325
410
  await sleep(100)
326
- return this.sendUpdate(json, retries + 1)
411
+ return this.sendRequest(json, retries + 1)
327
412
  }
328
413
  return parseError(err)
329
414
  }
@@ -449,13 +449,13 @@ export default class {
449
449
  this.cacheG = params.lightGcolor
450
450
  this.cacheB = params.lightBcolor
451
451
 
452
- // Get the Hue information from the RGB colour (saturation is always 100)
452
+ // Get the hue and saturation from the RGB colour
453
453
  const newColour = rgb2hs(this.cacheR, this.cacheG, this.cacheB);
454
454
  [this.cacheHue] = newColour
455
455
 
456
456
  // Update the HomeKit values
457
457
  this.lightService.updateCharacteristic(this.hapChar.Hue, this.cacheHue)
458
- this.lightService.updateCharacteristic(this.hapChar.Saturation, 100)
458
+ this.lightService.updateCharacteristic(this.hapChar.Saturation, newColour[1])
459
459
 
460
460
  // Log if appropriate
461
461
  if (params.updateSource && this.enableLogging) {
@@ -1,4 +1,4 @@
1
- import { generateRandomString, sleep } from '../utils/functions.js'
1
+ import { generateRandomString, hasProperty, sleep } from '../utils/functions.js'
2
2
 
3
3
  export default class {
4
4
  constructor(platform, accessory) {
@@ -41,9 +41,10 @@ export default class {
41
41
  }
42
42
 
43
43
  /*
44
- The device does not provide a current humidity reading so
45
- we use a fan accessory to be able to control the on/off state
46
- and the modes (1, 2, 3) using a rotation speed of (33%, 66%, 99%)
44
+ A fan accessory controls the on/off state and the modes (1, 2, 3) using a
45
+ rotation speed of (33%, 66%, 99%). CoolKit's protocol says the device also
46
+ reports the room humidity and temperature, so those get their own sensors,
47
+ added the first time the device sends them
47
48
  */
48
49
 
49
50
  // Add the fan service if it doesn't already exist
@@ -181,6 +182,38 @@ export default class {
181
182
  this.service.updateCharacteristic(this.hapChar.RotationSpeed, 0)
182
183
  }
183
184
  }
185
+ if (hasProperty(params, 'humidity')) {
186
+ const humi = Number.parseInt(params.humidity, 10)
187
+ if (!Number.isNaN(humi) && humi !== this.cacheHumi) {
188
+ this.cacheHumi = Math.max(Math.min(humi, 100), 0)
189
+ const humiService = this.accessory.getService(this.hapServ.HumiditySensor)
190
+ || this.accessory.addService(this.hapServ.HumiditySensor)
191
+ humiService.updateCharacteristic(this.hapChar.CurrentRelativeHumidity, this.cacheHumi)
192
+ if (params.updateSource && this.enableLogging) {
193
+ this.log('[%s] %s [%s%].', this.name, this.lang.curHumi, this.cacheHumi)
194
+ }
195
+ }
196
+ }
197
+ if (hasProperty(params, 'temperature')) {
198
+ const temp = Number(params.temperature)
199
+ if (!Number.isNaN(temp) && temp !== this.cacheTemp) {
200
+ this.cacheTemp = temp
201
+ const tempService = this.accessory.getService(this.hapServ.TemperatureSensor)
202
+ || this.accessory.addService(this.hapServ.TemperatureSensor)
203
+ tempService.updateCharacteristic(this.hapChar.CurrentTemperature, this.cacheTemp)
204
+ if (params.updateSource && this.enableLogging) {
205
+ this.log('[%s] %s [%s°C].', this.name, this.lang.curTemp, this.cacheTemp)
206
+ }
207
+ }
208
+ }
209
+ if (hasProperty(params, 'water')) {
210
+ // 0 means the tank is short of water, 1 that it is fine
211
+ const isEmpty = Number(params.water) === 0
212
+ if (isEmpty && !this.waterWarned) {
213
+ this.log.warn('[%s] %s.', this.name, this.lang.waterEmpty)
214
+ }
215
+ this.waterWarned = isEmpty
216
+ }
184
217
  if (params.state && params.state !== this.cacheMode) {
185
218
  this.cacheMode = params.state
186
219
  if (this.cacheState === 'on') {
@@ -27,6 +27,11 @@ export default class {
27
27
  : platformConsts.defaultValues.brightnessStep
28
28
  this.offlineAsOff = deviceConf.offlineAsOff
29
29
 
30
+ // UIID 52 uses an older protocol than UIID 103: `state` for on/off, then
31
+ // `channel0` for brightness (10-255) and `channel1` for colour temperature
32
+ // (0-255, higher is warmer), all as strings
33
+ this.useChannels = accessory.context.eweUIID === 52
34
+
30
35
  // Set the correct logging variables for this accessory
31
36
  switch (deviceConf.overrideLogging) {
32
37
  case 'standard':
@@ -164,9 +169,12 @@ export default class {
164
169
  this.updateTimeout = false
165
170
  }
166
171
  }, 5000)
167
- await this.platform.sendDeviceUpdate(this.accessory, {
168
- switch: newValue,
169
- })
172
+ await this.platform.sendDeviceUpdate(
173
+ this.accessory,
174
+ this.useChannels
175
+ ? { state: newValue }
176
+ : { switch: newValue },
177
+ )
170
178
  this.cacheState = newValue
171
179
  if (this.enableLogging) {
172
180
  this.log('[%s] %s [%s].', this.name, this.lang.curState, this.cacheState)
@@ -197,12 +205,16 @@ export default class {
197
205
  this.updateTimeout = false
198
206
  }
199
207
  }, 5000)
200
- const params = {
201
- white: {
202
- br: value,
203
- ct: this.cacheCT,
204
- },
205
- }
208
+ const params = this.useChannels
209
+ ? {
210
+ channel0: String(Math.round(10 + (Math.max(value, 1) / 100) * 245)),
211
+ }
212
+ : {
213
+ white: {
214
+ br: value,
215
+ ct: this.cacheCT,
216
+ },
217
+ }
206
218
  await this.platform.sendDeviceUpdate(this.accessory, params)
207
219
  this.cacheBright = value
208
220
  if (this.enableLogging) {
@@ -244,12 +256,16 @@ export default class {
244
256
  const kelvin = Math.round(1000000 / value)
245
257
  const scaledK = Math.max(Math.min(kelvin, this.maxK), this.minK)
246
258
  const scaledCT = Math.round(((scaledK - this.minK) / (this.maxK - this.minK)) * 255)
247
- const params = {
248
- white: {
249
- br: this.cacheBright,
250
- ct: scaledCT,
251
- },
252
- }
259
+ const params = this.useChannels
260
+ ? {
261
+ channel1: String(255 - scaledCT),
262
+ }
263
+ : {
264
+ white: {
265
+ br: this.cacheBright,
266
+ ct: scaledCT,
267
+ },
268
+ }
253
269
  await this.platform.sendDeviceUpdate(this.accessory, params)
254
270
  this.cacheMired = value
255
271
  this.cacheCT = scaledCT
@@ -274,6 +290,9 @@ export default class {
274
290
  if (this.updateTimeout) {
275
291
  return
276
292
  }
293
+ if (this.useChannels) {
294
+ params = this.channelsToWhite(params)
295
+ }
277
296
  if (params.switch && params.switch !== this.cacheState) {
278
297
  this.cacheState = params.switch
279
298
  this.service.updateCharacteristic(this.hapChar.On, this.cacheState === 'on')
@@ -319,6 +338,30 @@ export default class {
319
338
  }
320
339
  }
321
340
 
341
+ // Read a UIID 52 update in the shape of a UIID 103 one, so both share the rest
342
+ channelsToWhite(params) {
343
+ const white = {}
344
+ if (hasProperty(params, 'channel0')) {
345
+ const raw = Number.parseInt(params.channel0, 10)
346
+ if (!Number.isNaN(raw)) {
347
+ white.br = Math.min(Math.max(Math.round(((raw - 10) / 245) * 100), 1), 100)
348
+ }
349
+ }
350
+ if (hasProperty(params, 'channel1')) {
351
+ const raw = Number.parseInt(params.channel1, 10)
352
+ if (!Number.isNaN(raw)) {
353
+ white.ct = 255 - Math.min(Math.max(raw, 0), 255)
354
+ }
355
+ }
356
+ return {
357
+ ...params,
358
+ switch: params.state,
359
+ ...(Object.keys(white).length
360
+ ? { white }
361
+ : {}),
362
+ }
363
+ }
364
+
322
365
  markStatus(isOnline) {
323
366
  this.isOnline = isOnline
324
367
 
@@ -744,7 +744,7 @@ export default class {
744
744
  [this.cacheHue] = hs
745
745
  this.service.updateCharacteristic(this.hapChar.ColorTemperature, 140)
746
746
  this.service.updateCharacteristic(this.hapChar.Hue, this.cacheHue)
747
- this.service.updateCharacteristic(this.hapChar.Saturation, 100)
747
+ this.service.updateCharacteristic(this.hapChar.Saturation, hs[1])
748
748
  if (params.updateSource && this.enableLogging) {
749
749
  this.log(
750
750
  '[%s] %s [rgb %s].',
@@ -51,11 +51,19 @@ export default class {
51
51
  .getCharacteristic(this.hapChar.On)
52
52
  .onSet(async value => this.internalStateUpdate(value))
53
53
 
54
- // Add the set handler to the lightbulb brightness characteristic
54
+ // The B1 dims in white mode, with channel0 (cold) and channel1 (warm) from
55
+ // 25 to 255, and has three white tones: cold, middle and warm. CoolKit's
56
+ // protocol gives no brightness for colour mode, so there it scales the RGB
55
57
  this.service
56
58
  .getCharacteristic(this.hapChar.Brightness)
57
- .setProps({ minStep: 100 })
59
+ .setProps({ minStep: 1 })
58
60
  .onSet(async value => this.internalBrightnessUpdate(value))
61
+ this.cacheBright = this.service.getCharacteristic(this.hapChar.Brightness).value || 100
62
+
63
+ // Add the set handler to the lightbulb colour temperature characteristic
64
+ this.service
65
+ .getCharacteristic(this.hapChar.ColorTemperature)
66
+ .onSet(async value => this.internalCTUpdate(value))
59
67
 
60
68
  // Add the set handler to the lightbulb hue characteristic
61
69
  this.service
@@ -137,8 +145,29 @@ export default class {
137
145
 
138
146
  async internalBrightnessUpdate(value) {
139
147
  try {
140
- // B1 doesn't support brightness
148
+ if (value === 0 || this.cacheBright === value) {
149
+ return
150
+ }
151
+ const updateKeyBright = generateRandomString(5)
152
+ this.updateKeyBright = updateKeyBright
153
+ await sleep(500)
154
+ if (updateKeyBright !== this.updateKeyBright) {
155
+ return
156
+ }
157
+ this.updateTimeout = updateKeyBright
158
+ setTimeout(() => {
159
+ if (this.updateTimeout === updateKeyBright) {
160
+ this.updateTimeout = false
161
+ }
162
+ }, 5000)
163
+ const params = this.cacheMode === 2
164
+ ? this.colourParams(this.cacheHue, this.cacheSat, value)
165
+ : this.whiteParams(this.cacheType, value)
166
+ await this.platform.sendDeviceUpdate(this.accessory, params)
141
167
  this.cacheBright = value
168
+ if (this.enableLogging) {
169
+ this.log('[%s] %s [%s%].', this.name, this.lang.curBright, this.cacheBright)
170
+ }
142
171
  } catch (err) {
143
172
  this.platform.deviceUpdateError(this.accessory, err, true)
144
173
  setTimeout(() => {
@@ -169,20 +198,14 @@ export default class {
169
198
  }
170
199
  }, 5000)
171
200
  const saturation = this.service.getCharacteristic(this.hapChar.Saturation).value
172
- const newRGB = hs2rgb(value, saturation)
173
- const params = {
174
- zyx_mode: 2,
175
- type: 'middle',
176
- channel0: '0',
177
- channel1: '0',
178
- channel2: newRGB[0].toString(),
179
- channel3: newRGB[1].toString(),
180
- channel4: newRGB[2].toString(),
181
- }
201
+ const params = this.colourParams(value, saturation, this.cacheBright)
182
202
  await this.platform.sendDeviceUpdate(this.accessory, params)
183
203
  this.cacheHue = value
184
- this.cacheSat = saturation;
185
- [this.cacheR, this.cacheG, this.cacheB] = newRGB
204
+ this.cacheSat = saturation
205
+ this.cacheMode = 2
206
+ this.cacheR = Number(params.channel2)
207
+ this.cacheG = Number(params.channel3)
208
+ this.cacheB = Number(params.channel4)
186
209
  if (this.enableLogging) {
187
210
  this.log(
188
211
  '[%s] %s [rgb %s].',
@@ -200,6 +223,71 @@ export default class {
200
223
  }
201
224
  }
202
225
 
226
+ async internalCTUpdate(value) {
227
+ try {
228
+ // HomeKit's mireds, from 140 (cold) to 500 (warm), onto the three tones
229
+ const newType = value < 250
230
+ ? 'cold'
231
+ : value > 370
232
+ ? 'warm'
233
+ : 'middle'
234
+ if (this.cacheMode === 1 && this.cacheType === newType) {
235
+ return
236
+ }
237
+ const updateKeyCT = generateRandomString(5)
238
+ this.updateKeyCT = updateKeyCT
239
+ await sleep(400)
240
+ if (updateKeyCT !== this.updateKeyCT) {
241
+ return
242
+ }
243
+ this.updateTimeout = updateKeyCT
244
+ setTimeout(() => {
245
+ if (this.updateTimeout === updateKeyCT) {
246
+ this.updateTimeout = false
247
+ }
248
+ }, 5000)
249
+ await this.platform.sendDeviceUpdate(this.accessory, this.whiteParams(newType, this.cacheBright))
250
+ this.cacheType = newType
251
+ this.cacheMode = 1
252
+ if (this.enableLogging) {
253
+ this.log('[%s] %s [%s].', this.name, this.lang.curColour, newType)
254
+ }
255
+ } catch (err) {
256
+ this.platform.deviceUpdateError(this.accessory, err, true)
257
+ throw new this.hapErr(-70402)
258
+ }
259
+ }
260
+
261
+ // The white light, with HomeKit's 1-100 brightness on the channels' 25-255
262
+ whiteParams(type = 'middle', brightness = 100) {
263
+ const level = String(Math.round(25 + ((Math.max(brightness, 1) - 1) / 99) * 230))
264
+ return {
265
+ zyx_mode: 1,
266
+ type,
267
+ channel0: type === 'warm'
268
+ ? '0'
269
+ : level,
270
+ channel1: type === 'cold'
271
+ ? '0'
272
+ : level,
273
+ }
274
+ }
275
+
276
+ // A colour, with the brightness scaling the RGB, as colour mode has no brightness
277
+ colourParams(hue = 0, saturation = 100, brightness = 100) {
278
+ const scale = Math.max(brightness, 1) / 100
279
+ const rgb = hs2rgb(hue, saturation).map(x => Math.round(x * scale))
280
+ return {
281
+ zyx_mode: 2,
282
+ type: 'middle',
283
+ channel0: '0',
284
+ channel1: '0',
285
+ channel2: rgb[0].toString(),
286
+ channel3: rgb[1].toString(),
287
+ channel4: rgb[2].toString(),
288
+ }
289
+ }
290
+
203
291
  async externalUpdate(params) {
204
292
  try {
205
293
  if (this.updateTimeout) {
@@ -208,12 +296,6 @@ export default class {
208
296
  if (params.state && params.state !== this.cacheState) {
209
297
  this.cacheState = params.state
210
298
  this.service.updateCharacteristic(this.hapChar.On, this.cacheState === 'on')
211
- this.service.updateCharacteristic(
212
- this.hapChar.Brightness,
213
- this.cacheState === 'on'
214
- ? 100
215
- : 0,
216
- )
217
299
  if (params.updateSource && this.enableLogging) {
218
300
  this.log('[%s] %s [%s].', this.name, this.lang.curState, this.cacheState)
219
301
  }
@@ -230,7 +312,35 @@ export default class {
230
312
  } else {
231
313
  mode = 2
232
314
  }
315
+ if (mode === 1 && hasProperty(params, 'channel0')) {
316
+ const cold = Number.parseInt(params.channel0, 10) || 0
317
+ const warm = Number.parseInt(params.channel1, 10) || 0
318
+ const level = Math.max(cold, warm)
319
+ const type = cold > warm
320
+ ? 'cold'
321
+ : cold < warm
322
+ ? 'warm'
323
+ : 'middle'
324
+ this.cacheMode = 1
325
+ if (level >= 25) {
326
+ const bright = Math.min(Math.max(Math.round(((level - 25) / 230) * 99 + 1), 1), 100)
327
+ if (bright !== this.cacheBright) {
328
+ this.cacheBright = bright
329
+ this.service.updateCharacteristic(this.hapChar.Brightness, bright)
330
+ }
331
+ }
332
+ if (type !== this.cacheType) {
333
+ this.cacheType = type
334
+ // 6500K, 4000K and 2700K
335
+ const mired = { cold: 154, middle: 250, warm: 370 }[type]
336
+ this.service.updateCharacteristic(this.hapChar.ColorTemperature, mired)
337
+ if (params.updateSource && this.enableLogging) {
338
+ this.log('[%s] %s [%s].', this.name, this.lang.curColour, type)
339
+ }
340
+ }
341
+ }
233
342
  if (mode === 2) {
343
+ this.cacheMode = 2
234
344
  if (hasProperty(params, 'channel2')) {
235
345
  if (
236
346
  params.channel2 !== (this.cacheR || 0).toString()
@@ -246,7 +356,12 @@ export default class {
246
356
  // Saturation was pinned at 100 here, so a pale colour set in the
247
357
  // ewelink app came back to HomeKit as a fully saturated one
248
358
  this.service.updateCharacteristic(this.hapChar.Saturation, this.cacheSat)
249
- this.service.updateCharacteristic(this.hapChar.Brightness, 100)
359
+ // The brightest channel gives the brightness the colour was scaled by
360
+ const max = Math.max(this.cacheR, this.cacheG, this.cacheB)
361
+ if (max > 0) {
362
+ this.cacheBright = Math.max(Math.round((max / 255) * 100), 1)
363
+ this.service.updateCharacteristic(this.hapChar.Brightness, this.cacheBright)
364
+ }
250
365
  if (params.updateSource && this.enableLogging) {
251
366
  this.log(
252
367
  '[%s] %s [rgb %s].',
@@ -1,5 +1,5 @@
1
1
  import platformConsts from '../utils/constants.js'
2
- import { hasProperty, sleep, switchForChannel } from '../utils/functions.js'
2
+ import { hasProperty, phaseToChannelPower, sleep, switchForChannel } from '../utils/functions.js'
3
3
 
4
4
  export default class {
5
5
  constructor(platform, accessory, devicesInHB) {
@@ -68,7 +68,10 @@ export default class {
68
68
  if (platformConsts.devices.switchMultiPower.includes(this.accessory.context.eweUIID)) {
69
69
  // Add Eve power characteristics
70
70
  this.powerReadings = true
71
- this.isDual = true
71
+ // The matter metering switches report each channel's readings as phase_0X
72
+ this.usesPhases = [289, 290].includes(this.accessory.context.eweUIID)
73
+ // Most power models have two channels, but some (like uiid 262) have four
74
+ this.isDual = accessory.context.channelCount <= 2
72
75
  if (accessory.context.switchNumber !== '0') {
73
76
  if (!this.service.testCharacteristic(this.hapChar.OutletInUse)) {
74
77
  this.service.addCharacteristic(this.hapChar.OutletInUse)
@@ -334,6 +337,12 @@ export default class {
334
337
  return
335
338
  }
336
339
 
340
+ // The matter metering switches take one number for every channel
341
+ if (this.usesPhases) {
342
+ await this.platform.sendDeviceUpdate(this.accessory, { uiActive: 120 })
343
+ return
344
+ }
345
+
337
346
  // Send the params to request the updates
338
347
  await this.platform.sendDeviceUpdate(this.accessory, {
339
348
  uiActive: { outlet: 0, time: 120 },
@@ -349,6 +358,9 @@ export default class {
349
358
 
350
359
  async externalUpdate(params) {
351
360
  try {
361
+ if (this.usesPhases) {
362
+ phaseToChannelPower(params)
363
+ }
352
364
  const idToCheck = `${this.accessory.context.eweDeviceId}SW`
353
365
  let primaryState = false
354
366
  for (let i = 1; i <= this.accessory.context.channelCount; i += 1) {