@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 +23 -2
- package/lib/connection/http.js +23 -2
- package/lib/connection/lan.js +87 -2
- package/lib/device/diffuser.js +2 -2
- package/lib/device/humidifier.js +37 -4
- package/lib/device/light-cct.js +58 -15
- package/lib/device/light-rgb-cct.js +1 -1
- package/lib/device/light-rgb.js +137 -22
- package/lib/device/outlet-multi.js +14 -2
- package/lib/device/outlet-single-inched.js +41 -34
- package/lib/device/outlet-single.js +36 -32
- package/lib/device/sensor-temp-humi.js +10 -5
- package/lib/device/switch-multi.js +15 -3
- package/lib/device/switch-single-inched.js +7 -3
- package/lib/device/switch-single.js +27 -1
- package/lib/device/zigbee/motor.js +8 -0
- package/lib/device/zigbee/thermostat.js +28 -4
- package/lib/platform.js +5 -1
- package/lib/utils/constants.js +16 -3
- package/lib/utils/functions.js +27 -0
- package/lib/utils/lang-en.js +3 -0
- package/lib/utils/lang-fr.js +1 -0
- package/lib/utils/lang-th.js +1 -0
- package/package.json +6 -6
- package/CLAUDE.md +0 -144
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.
|
|
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
|
|
package/lib/connection/http.js
CHANGED
|
@@ -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
|
-
//
|
|
87
|
-
this.log.debug(
|
|
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')
|
package/lib/connection/lan.js
CHANGED
|
@@ -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
|
|
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.
|
|
411
|
+
return this.sendRequest(json, retries + 1)
|
|
327
412
|
}
|
|
328
413
|
return parseError(err)
|
|
329
414
|
}
|
package/lib/device/diffuser.js
CHANGED
|
@@ -449,13 +449,13 @@ export default class {
|
|
|
449
449
|
this.cacheG = params.lightGcolor
|
|
450
450
|
this.cacheB = params.lightBcolor
|
|
451
451
|
|
|
452
|
-
// Get the
|
|
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,
|
|
458
|
+
this.lightService.updateCharacteristic(this.hapChar.Saturation, newColour[1])
|
|
459
459
|
|
|
460
460
|
// Log if appropriate
|
|
461
461
|
if (params.updateSource && this.enableLogging) {
|
package/lib/device/humidifier.js
CHANGED
|
@@ -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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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') {
|
package/lib/device/light-cct.js
CHANGED
|
@@ -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(
|
|
168
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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,
|
|
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].',
|
package/lib/device/light-rgb.js
CHANGED
|
@@ -51,11 +51,19 @@ export default class {
|
|
|
51
51
|
.getCharacteristic(this.hapChar.On)
|
|
52
52
|
.onSet(async value => this.internalStateUpdate(value))
|
|
53
53
|
|
|
54
|
-
//
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|