@homebridge-plugins/homebridge-ewelink 12.19.1-beta.4 → 12.19.1-beta.5

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
@@ -15,6 +15,18 @@ All notable changes to `@homebridge-plugins/homebridge-ewelink` will be document
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
17
  - chore(deps): dependency updates
18
+ - fix: stop live updates dropping readings like light level, child lock and fan oscillation
19
+ - fix: show power readings on the 226 and 7032 when they are shown as an outlet
20
+ - fix: read inched power devices in hundredths, so a powr316 or s61 no longer shows 5500w for 55w
21
+ - fix: log in again when the cloud reports an expired token, so group commands keep working
22
+ - feat(zigbee): add the snzb-02b and snzb-02m temperature and humidity sensors
23
+ - feat(zigbee): add the snzb-06p24 presence sensor, with occupancy and light level
24
+ - feat(zigbee): add the mini-zb1gp switch, with power readings
25
+ - feat: add the mini-1gsp switch, with power readings
26
+ - feat: add the nspanel86pb relay, as a two-channel switch
27
+ - feat(zigbee): add the second generation trvzb radiator valve, on the existing thermostat
28
+ - feat(zigbee): add the swv-zf2e two-zone irrigation valve, with a valve for each zone
29
+ - feat: show pm2.5 and pm10 on the air quality sensors that measure them
18
30
 
19
31
  ## v12.19.0 (2026-10-18)
20
32
 
@@ -34,15 +34,22 @@ export default class {
34
34
  // A timeout surfaces as ECONNABORTED/ETIMEDOUT, which the retry logic handles.
35
35
  this.axiosClient = axios.create({ timeout: 30000 })
36
36
  this.axiosClient.interceptors.response.use(
37
- res => res,
37
+ async (res) => {
38
+ // eWeLink normally reports an expired token inside an ordinary response,
39
+ // as error 401 or 402, rather than as an http 401
40
+ if ([401, 402].includes(res.data?.error) && !res.config._retried) {
41
+ try {
42
+ return await this.retryWithNewToken(res.config)
43
+ } catch (loginErr) {
44
+ return res
45
+ }
46
+ }
47
+ return res
48
+ },
38
49
  async (err) => {
39
50
  if (err.response?.status === 401 && !err.config._retried) {
40
- err.config._retried = true
41
51
  try {
42
- const authData = await this.login()
43
- this.aToken = authData.aToken
44
- err.config.headers.Authorization = `Bearer ${this.aToken}`
45
- return this.axiosClient(err.config)
52
+ return await this.retryWithNewToken(err.config)
46
53
  } catch (loginErr) {
47
54
  throw err
48
55
  }
@@ -52,6 +59,15 @@ export default class {
52
59
  )
53
60
  }
54
61
 
62
+ async retryWithNewToken(config) {
63
+ // Log in again for a fresh token, then send the request once more
64
+ config._retried = true
65
+ const authData = await this.login()
66
+ this.aToken = authData.aToken
67
+ config.headers.Authorization = `Bearer ${this.aToken}`
68
+ return this.axiosClient(config)
69
+ }
70
+
55
71
  async login() {
56
72
  try {
57
73
  // Used to log the user in and obtain the user api key and token
@@ -61,6 +61,9 @@ export default class {
61
61
  this.service = this.accessory.getService(this.hapServ.Outlet)
62
62
  || this.accessory.addService(this.hapServ.Outlet)
63
63
 
64
+ // Some models provide a power reading in different decimals
65
+ this.divisor = 1
66
+
64
67
  switch (this.accessory.context.eweUIID) {
65
68
  case 5:
66
69
  // Add Eve power characteristics
@@ -91,7 +94,11 @@ export default class {
91
94
  case 32:
92
95
  case 182:
93
96
  case 190:
97
+ case 226:
94
98
  case 276:
99
+ case 283:
100
+ case 7032:
101
+ case 7054:
95
102
  // Add Eve power characteristics
96
103
  this.powerReadings = true
97
104
  if (!this.service.testCharacteristic(this.hapChar.OutletInUse)) {
@@ -112,6 +119,10 @@ export default class {
112
119
  if (!this.service.testCharacteristic(this.eveChar.ResetTotal)) {
113
120
  this.service.addCharacteristic(this.eveChar.ResetTotal)
114
121
  }
122
+
123
+ if ([190, 276, 283, 7032, 7054].includes(this.accessory.context.eweUIID)) {
124
+ this.divisor = 100
125
+ }
115
126
  break
116
127
  default:
117
128
  // Remove unused Eve characteristics
@@ -355,6 +366,24 @@ export default class {
355
366
  if (!this.powerReadings) {
356
367
  return
357
368
  }
369
+
370
+ // Some models (e.g. uiid 226) report their readings under phase names
371
+ if (hasProperty(params, 'phase_0_p')) {
372
+ params.power = params.phase_0_p
373
+ }
374
+ if (hasProperty(params, 'phase_0_v')) {
375
+ params.voltage = params.phase_0_v
376
+ }
377
+ if (hasProperty(params, 'phase_0_c')) {
378
+ params.current = params.phase_0_c
379
+ }
380
+
381
+ // Some models report their readings in hundredths
382
+ for (const reading of ['power', 'voltage', 'current']) {
383
+ if (hasProperty(params, reading)) {
384
+ params[reading] = Math.round((Number.parseFloat(params[reading]) * 100) / this.divisor) / 100
385
+ }
386
+ }
358
387
  let logger = false
359
388
  if (hasProperty(params, 'power')) {
360
389
  const power = Number.parseFloat(params.power)
@@ -94,7 +94,11 @@ export default class {
94
94
  case 32:
95
95
  case 182:
96
96
  case 190:
97
+ case 226:
97
98
  case 276:
99
+ case 283:
100
+ case 7032:
101
+ case 7054:
98
102
  // Add Eve power characteristics
99
103
  this.powerReadings = true
100
104
  if (!this.service.testCharacteristic(this.hapChar.OutletInUse)) {
@@ -115,7 +119,7 @@ export default class {
115
119
  if (!this.service.testCharacteristic(this.eveChar.ResetTotal)) {
116
120
  this.service.addCharacteristic(this.eveChar.ResetTotal)
117
121
  }
118
- if ([190, 276].includes(this.accessory.context.eweUIID)) {
122
+ if ([190, 276, 283, 7032, 7054].includes(this.accessory.context.eweUIID)) {
119
123
  this.divisor = 100
120
124
  }
121
125
  break
@@ -329,6 +333,17 @@ export default class {
329
333
  if (!this.powerReadings) {
330
334
  return
331
335
  }
336
+
337
+ // Some models (e.g. uiid 226) report their readings under phase names
338
+ if (hasProperty(params, 'phase_0_p')) {
339
+ params.power = params.phase_0_p
340
+ }
341
+ if (hasProperty(params, 'phase_0_v')) {
342
+ params.voltage = params.phase_0_v
343
+ }
344
+ if (hasProperty(params, 'phase_0_c')) {
345
+ params.current = params.phase_0_c
346
+ }
332
347
  let logger = false
333
348
  let power
334
349
  let voltage
@@ -205,6 +205,40 @@ export default class {
205
205
  }
206
206
  }
207
207
  }
208
+
209
+ // Some models (e.g. SAWF-07P) also measure particles, in µg/m³. The
210
+ // readings are only added to the sensor once the device reports them.
211
+ const particles = [
212
+ [
213
+ 'pm2_5',
214
+ 'cachePM25',
215
+ this.hapChar.PM2_5Density,
216
+ this.lang.curPM25,
217
+ ],
218
+ [
219
+ 'pm10',
220
+ 'cachePM10',
221
+ this.hapChar.PM10Density,
222
+ this.lang.curPM10,
223
+ ],
224
+ ]
225
+ particles.forEach(([param, cacheKey, char, label]) => {
226
+ if (!hasProperty(params, param)) {
227
+ return
228
+ }
229
+ const newValue = Math.max(Math.min(Number.parseInt(params[param], 10), 1000), 0)
230
+ if (Number.isNaN(newValue) || newValue === this[cacheKey]) {
231
+ return
232
+ }
233
+ this[cacheKey] = newValue
234
+ if (!this.airQualityService.testCharacteristic(char)) {
235
+ this.airQualityService.addCharacteristic(char)
236
+ }
237
+ this.airQualityService.updateCharacteristic(char, newValue)
238
+ if (params.updateSource && this.enableLogging) {
239
+ this.log('[%s] %s [%s µg/m³].', this.name, label, newValue)
240
+ }
241
+ })
208
242
  } catch (err) {
209
243
  this.platform.deviceUpdateError(this.accessory, err, false)
210
244
  }
@@ -61,6 +61,9 @@ export default class {
61
61
  this.service = this.accessory.getService(this.hapServ.Switch)
62
62
  || this.accessory.addService(this.hapServ.Switch)
63
63
 
64
+ // Some models provide a power reading in different decimals
65
+ this.divisor = 1
66
+
64
67
  switch (this.accessory.context.eweUIID) {
65
68
  case 5:
66
69
  // Add Eve power characteristics
@@ -85,6 +88,11 @@ export default class {
85
88
  case 32:
86
89
  case 182:
87
90
  case 190:
91
+ case 226:
92
+ case 276:
93
+ case 283:
94
+ case 7032:
95
+ case 7054:
88
96
  // Add Eve power characteristics
89
97
  this.powerReadings = true
90
98
  if (!this.service.testCharacteristic(this.hapChar.OutletInUse)) {
@@ -99,6 +107,10 @@ export default class {
99
107
  if (!this.service.testCharacteristic(this.eveChar.Voltage)) {
100
108
  this.service.addCharacteristic(this.eveChar.Voltage)
101
109
  }
110
+
111
+ if ([190, 276, 283, 7032, 7054].includes(this.accessory.context.eweUIID)) {
112
+ this.divisor = 100
113
+ }
102
114
  break
103
115
  default:
104
116
  // Remove unused Eve characteristics
@@ -283,6 +295,24 @@ export default class {
283
295
  if (!this.powerReadings) {
284
296
  return
285
297
  }
298
+
299
+ // Some models (e.g. uiid 226) report their readings under phase names
300
+ if (hasProperty(params, 'phase_0_p')) {
301
+ params.power = params.phase_0_p
302
+ }
303
+ if (hasProperty(params, 'phase_0_v')) {
304
+ params.voltage = params.phase_0_v
305
+ }
306
+ if (hasProperty(params, 'phase_0_c')) {
307
+ params.current = params.phase_0_c
308
+ }
309
+
310
+ // Some models report their readings in hundredths
311
+ for (const reading of ['power', 'voltage', 'current']) {
312
+ if (hasProperty(params, reading)) {
313
+ params[reading] = Math.round((Number.parseFloat(params[reading]) * 100) / this.divisor) / 100
314
+ }
315
+ }
286
316
  let logger = false
287
317
  if (hasProperty(params, 'power')) {
288
318
  this.service.updateCharacteristic(this.eveChar.CurrentConsumption, Number.parseFloat(params.power))
@@ -90,7 +90,9 @@ export default class {
90
90
  case 190:
91
91
  case 226:
92
92
  case 276:
93
+ case 283:
93
94
  case 7032:
95
+ case 7054:
94
96
  // Add Eve power characteristics
95
97
  this.powerReadings = true
96
98
  if (!this.service.testCharacteristic(this.hapChar.OutletInUse)) {
@@ -106,7 +108,7 @@ export default class {
106
108
  this.service.addCharacteristic(this.eveChar.Voltage)
107
109
  }
108
110
 
109
- if ([190, 276, 7032].includes(this.accessory.context.eweUIID)) {
111
+ if ([190, 276, 283, 7032, 7054].includes(this.accessory.context.eweUIID)) {
110
112
  this.divisor = 100
111
113
  }
112
114
  break
@@ -54,8 +54,8 @@ export default class {
54
54
  || this.accessory.addService(this.hapServ.Battery)
55
55
 
56
56
  // SNZB-06P (uiid 7016) also reports a binary bright/dark light level, and
57
- // SNZB-03PR2 (uiid 7055) a numeric illumination reading
58
- if ([7016, 7055].includes(accessory.context.eweUIID)) {
57
+ // SNZB-06P24 (uiid 7051) and SNZB-03PR2 (uiid 7055) a numeric illumination reading
58
+ if ([7016, 7051, 7055].includes(accessory.context.eweUIID)) {
59
59
  this.lightService = this.accessory.getService(this.hapServ.LightSensor)
60
60
  || this.accessory.addService(this.hapServ.LightSensor)
61
61
  } else if (this.accessory.getService(this.hapServ.LightSensor)) {
@@ -35,158 +35,182 @@ export default class {
35
35
  }
36
36
 
37
37
  // The newer SWV-ZNE irrigation valve (uiid 7047) reports and accepts its
38
- // state under 'switch_00' rather than 'switch' (#778)
39
- this.switchParam = accessory.context.eweUIID === 7047
40
- ? 'switch_00'
41
- : 'switch'
38
+ // state under 'switch_00' rather than 'switch' (#778). The two-zone
39
+ // SWV-ZF2E (uiid 7048) does the same, with its second valve on 'switch_01'.
40
+ if (accessory.context.eweUIID === 7048) {
41
+ this.zones = [
42
+ { param: 'switch_00', label: 'Valve 1', subtype: 'valve1' },
43
+ { param: 'switch_01', label: 'Valve 2', subtype: 'valve2' },
44
+ ]
45
+ } else {
46
+ this.zones = [
47
+ {
48
+ param: accessory.context.eweUIID === 7047
49
+ ? 'switch_00'
50
+ : 'switch',
51
+ },
52
+ ]
53
+ }
54
+ this.zones.forEach(zone => this.setUpZone(zone))
55
+
56
+ this.log('[%s] initialized as a Zigbee Smart Water Valve.', this.name)
57
+ }
42
58
 
43
- // Initialize cache state
44
- this.cacheState = false
59
+ setUpZone(zone) {
60
+ // Initialize cache state and timer references
61
+ zone.cacheState = false
62
+ zone.timer = null
63
+ zone.updateInterval = null
45
64
 
46
- // Initialize timer references
47
- this.timer = null
48
- this.updateInterval = null
65
+ // A single valve keeps the service, name and saved timer it has always had
66
+ zone.name = zone.label
67
+ ? `${this.name} ${zone.label}`
68
+ : this.name
69
+ zone.timerKey = zone.subtype
70
+ ? `timerEndTime_${zone.subtype}`
71
+ : 'timerEndTime'
49
72
 
50
73
  // Set up the Valve Service for HomeKit
51
- this.service = this.accessory.getService(this.hapServ.Valve)
52
- if (!this.service) {
53
- this.service = this.accessory.addService(this.hapServ.Valve)
54
- this.service.updateCharacteristic(this.hapChar.Active, 0)
55
- this.service.updateCharacteristic(this.hapChar.InUse, 0)
56
- this.service.updateCharacteristic(this.hapChar.ValveType, 1) // 1 = Irrigation
57
- this.service.updateCharacteristic(this.hapChar.SetDuration, 30) // Default timer duration
58
- this.service.addCharacteristic(this.hapChar.RemainingDuration)
74
+ zone.service = zone.subtype
75
+ ? this.accessory.getServiceById(this.hapServ.Valve, zone.subtype)
76
+ : this.accessory.getService(this.hapServ.Valve)
77
+ if (!zone.service) {
78
+ zone.service = zone.subtype
79
+ ? this.accessory.addService(this.hapServ.Valve, zone.label, zone.subtype)
80
+ : this.accessory.addService(this.hapServ.Valve)
81
+ zone.service.updateCharacteristic(this.hapChar.Active, 0)
82
+ zone.service.updateCharacteristic(this.hapChar.InUse, 0)
83
+ zone.service.updateCharacteristic(this.hapChar.ValveType, 1) // 1 = Irrigation
84
+ zone.service.updateCharacteristic(this.hapChar.SetDuration, 30) // Default timer duration
85
+ zone.service.addCharacteristic(this.hapChar.RemainingDuration)
59
86
  }
60
87
 
61
88
  // Restore timer state if it exists
62
- if (this.accessory.context.timerEndTime) {
89
+ if (this.accessory.context[zone.timerKey]) {
63
90
  const now = Date.now()
64
- if (now < this.accessory.context.timerEndTime) {
91
+ if (now < this.accessory.context[zone.timerKey]) {
65
92
  // Timer should still be running
66
- const remainingTime = Math.round((this.accessory.context.timerEndTime - now) / 1000)
67
- this.service.updateCharacteristic(this.hapChar.RemainingDuration, remainingTime)
68
- this.startTimer(remainingTime)
93
+ const remainingTime = Math.round((this.accessory.context[zone.timerKey] - now) / 1000)
94
+ zone.service.updateCharacteristic(this.hapChar.RemainingDuration, remainingTime)
95
+ this.startTimer(zone, remainingTime)
69
96
  } else {
70
97
  // Timer has expired, clean up
71
- this.service.updateCharacteristic(this.hapChar.Active, 0)
72
- this.service.updateCharacteristic(this.hapChar.InUse, 0)
73
- this.service.updateCharacteristic(this.hapChar.RemainingDuration, 0)
74
- delete this.accessory.context.timerEndTime
98
+ zone.service.updateCharacteristic(this.hapChar.Active, 0)
99
+ zone.service.updateCharacteristic(this.hapChar.InUse, 0)
100
+ zone.service.updateCharacteristic(this.hapChar.RemainingDuration, 0)
101
+ delete this.accessory.context[zone.timerKey]
75
102
  }
76
103
  }
77
104
 
78
105
  // Set up event handlers for Active characteristic
79
- this.service
106
+ zone.service
80
107
  .getCharacteristic(this.hapChar.Active)
81
108
  .onSet(async (value) => {
82
- const prevState = this.cacheState
109
+ const prevState = zone.cacheState
83
110
  try {
84
111
  const newState = value === 1
85
112
 
86
113
  // HomeKit can send the same command several times over, so ignore
87
114
  // anything that asks the valve to do what it is already doing (#778)
88
- if (newState === this.cacheState) {
115
+ if (newState === zone.cacheState) {
89
116
  return
90
117
  }
91
118
 
92
119
  // Claim the new state before sending, so repeats that arrive while
93
120
  // this command is still in flight are ignored too
94
- this.cacheState = newState
121
+ zone.cacheState = newState
95
122
 
96
123
  // The device can echo a superseded state back after a newer command
97
124
  // has been sent, flipping HomeKit to a state we have already left
98
125
  // behind - suppress incoming updates around a command, the same way
99
126
  // the other device types do (#778)
100
- const timerKey = generateRandomString(5)
101
- this.updateTimeout = timerKey
102
- setTimeout(() => {
103
- if (this.updateTimeout === timerKey) {
104
- this.updateTimeout = false
105
- }
106
- }, 5000)
127
+ this.suppressUpdates(zone)
107
128
 
108
129
  if (this.enableLogging) {
109
- this.log('[%s] Setting valve to [%s]', this.name, newState ? 'On' : 'Off')
130
+ this.log('[%s] Setting valve to [%s]', zone.name, newState ? 'On' : 'Off')
110
131
  }
111
132
 
112
133
  // Send command to the device
113
134
  await this.platform.sendDeviceUpdate(this.accessory, {
114
- [this.switchParam]: newState,
135
+ [zone.param]: newState,
115
136
  })
116
137
 
117
138
  // Update InUse characteristic to match Active
118
- this.service.updateCharacteristic(this.hapChar.InUse, value)
139
+ zone.service.updateCharacteristic(this.hapChar.InUse, value)
119
140
 
120
141
  if (value === 1) {
121
142
  // If turning on, start the timer
122
- const duration = this.service.getCharacteristic(this.hapChar.SetDuration).value
123
- this.startTimer(duration)
143
+ const duration = zone.service.getCharacteristic(this.hapChar.SetDuration).value
144
+ this.startTimer(zone, duration)
124
145
  } else {
125
146
  // If turning off, clear the timer and reset remaining duration
126
- this.clearTimer()
147
+ this.clearTimer(zone)
127
148
  }
128
149
  } catch (err) {
129
150
  // Revert the characteristic state on error
130
- this.cacheState = prevState
151
+ zone.cacheState = prevState
131
152
  const prevValue = prevState ? 1 : 0
132
- this.service.updateCharacteristic(this.hapChar.Active, prevValue)
133
- this.service.updateCharacteristic(this.hapChar.InUse, prevValue)
153
+ zone.service.updateCharacteristic(this.hapChar.Active, prevValue)
154
+ zone.service.updateCharacteristic(this.hapChar.InUse, prevValue)
134
155
  this.platform.deviceUpdateError(this.accessory, err, false)
135
156
  throw new this.hapErr(-70402)
136
157
  }
137
158
  })
138
159
 
139
160
  // Set up handler for SetDuration characteristic
140
- this.service.getCharacteristic(this.hapChar.SetDuration)
161
+ zone.service.getCharacteristic(this.hapChar.SetDuration)
141
162
  .onSet((value) => {
142
- if (this.service.getCharacteristic(this.hapChar.InUse).value === 1) {
163
+ if (zone.service.getCharacteristic(this.hapChar.InUse).value === 1) {
143
164
  // Update timer with new duration if valve is currently active
144
- this.startTimer(value)
165
+ this.startTimer(zone, value)
145
166
  }
146
167
  })
168
+ }
147
169
 
148
- this.log('[%s] initialized as a Zigbee Smart Water Valve.', this.name)
170
+ suppressUpdates(zone) {
171
+ // Ignore this valve's incoming state for a few seconds
172
+ const timerKey = generateRandomString(5)
173
+ zone.updateTimeout = timerKey
174
+ setTimeout(() => {
175
+ if (zone.updateTimeout === timerKey) {
176
+ zone.updateTimeout = false
177
+ }
178
+ }, 5000)
149
179
  }
150
180
 
151
- startTimer(duration) {
181
+ startTimer(zone, duration) {
152
182
  // Clear any existing timer
153
- this.clearTimer()
183
+ this.clearTimer(zone)
154
184
 
155
185
  // Set timer end time and store it in context
156
- this.accessory.context.timerEndTime = Date.now() + (duration * 1000)
186
+ this.accessory.context[zone.timerKey] = Date.now() + (duration * 1000)
157
187
 
158
188
  // Update remaining duration
159
- this.service.updateCharacteristic(this.hapChar.RemainingDuration, duration)
189
+ zone.service.updateCharacteristic(this.hapChar.RemainingDuration, duration)
160
190
 
161
191
  // Set new timer
162
- this.timer = setTimeout(async () => {
192
+ zone.timer = setTimeout(async () => {
163
193
  try {
164
194
  // Suppress incoming updates around this command too, so an echo of
165
195
  // the earlier On cannot land after the Off we are about to send (#778)
166
- const timerKey = generateRandomString(5)
167
- this.updateTimeout = timerKey
168
- setTimeout(() => {
169
- if (this.updateTimeout === timerKey) {
170
- this.updateTimeout = false
171
- }
172
- }, 5000)
196
+ this.suppressUpdates(zone)
173
197
 
174
198
  // Send explicit command to turn off the valve
175
199
  await this.platform.sendDeviceUpdate(this.accessory, {
176
- [this.switchParam]: false,
200
+ [zone.param]: false,
177
201
  })
178
202
 
179
203
  // Update HomeKit characteristics
180
- this.service.updateCharacteristic(this.hapChar.Active, 0)
181
- this.service.updateCharacteristic(this.hapChar.InUse, 0)
182
- this.service.updateCharacteristic(this.hapChar.RemainingDuration, 0)
183
- delete this.accessory.context.timerEndTime
204
+ zone.service.updateCharacteristic(this.hapChar.Active, 0)
205
+ zone.service.updateCharacteristic(this.hapChar.InUse, 0)
206
+ zone.service.updateCharacteristic(this.hapChar.RemainingDuration, 0)
207
+ delete this.accessory.context[zone.timerKey]
184
208
 
185
209
  // Update cache state
186
- this.cacheState = false
210
+ zone.cacheState = false
187
211
 
188
212
  if (this.enableLogging) {
189
- this.log('[%s] Timer finished - turning valve [OFF]', this.name)
213
+ this.log('[%s] Timer finished - turning valve [OFF]', zone.name)
190
214
  }
191
215
  } catch (err) {
192
216
  this.platform.deviceUpdateError(this.accessory, err, false)
@@ -194,69 +218,71 @@ export default class {
194
218
  }, duration * 1000)
195
219
 
196
220
  // Start a periodic update of remaining duration
197
- if (this.updateInterval) {
198
- clearInterval(this.updateInterval)
199
- this.updateInterval = null
221
+ if (zone.updateInterval) {
222
+ clearInterval(zone.updateInterval)
223
+ zone.updateInterval = null
200
224
  }
201
- this.updateInterval = setInterval(() => {
202
- const remaining = Math.round((this.accessory.context.timerEndTime - Date.now()) / 1000)
225
+ zone.updateInterval = setInterval(() => {
226
+ const remaining = Math.round((this.accessory.context[zone.timerKey] - Date.now()) / 1000)
203
227
  if (remaining > 0) {
204
- this.service.updateCharacteristic(this.hapChar.RemainingDuration, remaining)
228
+ zone.service.updateCharacteristic(this.hapChar.RemainingDuration, remaining)
205
229
  } else {
206
- clearInterval(this.updateInterval)
207
- this.updateInterval = null
230
+ clearInterval(zone.updateInterval)
231
+ zone.updateInterval = null
208
232
  }
209
233
  }, 1000)
210
234
  }
211
235
 
212
- clearTimer() {
213
- if (this.timer) {
214
- clearTimeout(this.timer)
215
- this.timer = null
236
+ clearTimer(zone) {
237
+ if (zone.timer) {
238
+ clearTimeout(zone.timer)
239
+ zone.timer = null
216
240
  }
217
- if (this.updateInterval) {
218
- clearInterval(this.updateInterval)
219
- this.updateInterval = null
241
+ if (zone.updateInterval) {
242
+ clearInterval(zone.updateInterval)
243
+ zone.updateInterval = null
220
244
  }
221
- delete this.accessory.context.timerEndTime
222
- this.service.updateCharacteristic(this.hapChar.RemainingDuration, 0)
245
+ delete this.accessory.context[zone.timerKey]
246
+ zone.service.updateCharacteristic(this.hapChar.RemainingDuration, 0)
223
247
  }
224
248
 
225
249
  async externalUpdate(params) {
226
250
  try {
227
- // Ignore updates while a command we sent is settling - they can carry
228
- // the state we have just moved away from (#778)
229
- if (this.updateTimeout) {
230
- return
231
- }
251
+ this.zones.forEach((zone) => {
252
+ // Ignore updates while a command we sent is settling - they can carry
253
+ // the state we have just moved away from (#778)
254
+ if (zone.updateTimeout || !hasProperty(params, zone.param)) {
255
+ return
256
+ }
232
257
 
233
- // Handle On/Off state
234
- if (hasProperty(params, this.switchParam)) {
235
- const newState = !!params[this.switchParam]
236
- if (newState !== this.cacheState) {
237
- this.cacheState = newState
258
+ // Handle On/Off state
259
+ const newState = !!params[zone.param]
260
+ if (newState !== zone.cacheState) {
261
+ zone.cacheState = newState
238
262
  const value = newState ? 1 : 0
239
- this.service.updateCharacteristic(this.hapChar.Active, value)
240
- this.service.updateCharacteristic(this.hapChar.InUse, value)
263
+ zone.service.updateCharacteristic(this.hapChar.Active, value)
264
+ zone.service.updateCharacteristic(this.hapChar.InUse, value)
241
265
 
242
266
  if (!newState) {
243
267
  // If turned off externally, clear timer
244
- this.clearTimer()
268
+ this.clearTimer(zone)
245
269
  }
246
270
 
247
271
  if (params.updateSource && this.enableLogging) {
248
- this.log('[%s] Valve state updated to [%s]', this.name, newState ? 'On' : 'Off')
272
+ this.log('[%s] Valve state updated to [%s]', zone.name, newState ? 'On' : 'Off')
249
273
  }
250
274
  }
251
- }
275
+ })
252
276
  } catch (err) {
253
277
  this.platform.deviceUpdateError(this.accessory, err, false)
254
278
  }
255
279
  }
256
280
 
257
281
  destroy() {
258
- clearTimeout(this.timer)
259
- clearInterval(this.updateInterval)
282
+ this.zones.forEach((zone) => {
283
+ clearTimeout(zone.timer)
284
+ clearInterval(zone.updateInterval)
285
+ })
260
286
  }
261
287
 
262
288
  markStatus(isOnline) {
@@ -363,7 +363,7 @@ export default {
363
363
  209,
364
364
  264,
365
365
  ],
366
- switchSCMPower: [182, 190, 276, 7032],
366
+ switchSCMPower: [182, 190, 276, 283, 7032, 7054],
367
367
  switchSinglePower: [5, 32],
368
368
  switchMulti: [
369
369
  2,
@@ -392,6 +392,7 @@ export default {
392
392
  212,
393
393
  262,
394
394
  275,
395
+ 278,
395
396
  2256,
396
397
  3256,
397
398
  4256,
@@ -433,15 +434,24 @@ export default {
433
434
  zbLightCCT: [1258],
434
435
  zbLightRGBCCT: [3258, 7009],
435
436
  zbMotor: [1514, 7006, 7034],
436
- zbSensorAmbient: [242, 1770, 1771, 7014, 7033, 7038],
437
+ zbSensorAmbient: [
438
+ 242,
439
+ 1770,
440
+ 1771,
441
+ 7014,
442
+ 7033,
443
+ 7038,
444
+ 7052,
445
+ 7057,
446
+ ],
437
447
  zbSensorMotion: [2026, 7002],
438
- zbSensorOccupancy: [7016, 7055],
448
+ zbSensorOccupancy: [7016, 7051, 7055],
439
449
  zbSensorContact: [3026, 7003],
440
450
  zbSensorWater: [4026, 7019],
441
451
  zbSensorSmoke: [5026],
442
452
  zbSiren: [7056],
443
- zbThermostat: [7017],
444
- zbWaterValve: [7027, 7047],
453
+ zbThermostat: [7017, 7049],
454
+ zbWaterValve: [7027, 7047, 7048],
445
455
  group: [5000],
446
456
  template: [], // Currently unused
447
457
  cannotSupport: [
@@ -462,12 +472,15 @@ export default {
462
472
  paramsToKeep: [
463
473
  'actionTime',
464
474
  'actPow_',
475
+ 'alarmSetting',
465
476
  'battery',
466
477
  'bright',
478
+ 'brState',
467
479
  'brightness',
468
480
  'calibState',
469
481
  'cctBrightness',
470
482
  'channel',
483
+ 'childLock',
471
484
  'cmd',
472
485
  'co2',
473
486
  'color',
@@ -484,6 +497,7 @@ export default {
484
497
  'currentTemperature',
485
498
  'current_',
486
499
  'currLocation',
500
+ 'dusty',
487
501
  'autoTargetTemp',
488
502
  'curTargetTemp',
489
503
  'manTargetTemp',
@@ -491,6 +505,8 @@ export default {
491
505
  'hue',
492
506
  'human',
493
507
  'humidity',
508
+ 'illumination',
509
+ 'indoor_temperature',
494
510
  'key',
495
511
  'light',
496
512
  'lightbright',
@@ -509,6 +525,11 @@ export default {
509
525
  'online',
510
526
  'outlet',
511
527
  'per',
528
+ 'pm10',
529
+ 'pm2_5',
530
+ 'phase_0_c',
531
+ 'phase_0_p',
532
+ 'phase_0_v',
512
533
  'percentageControl',
513
534
  'power',
514
535
  'op',
@@ -518,11 +539,13 @@ export default {
518
539
  'rgbBrightness',
519
540
  'saturation',
520
541
  'setclose',
542
+ 'shake',
521
543
  'smoke',
522
544
  'speed',
523
545
  'state',
524
546
  'switch',
525
547
  'switch_00',
548
+ 'switch_01',
526
549
  'switches',
527
550
  'targetTemp',
528
551
  'temperature',
@@ -533,6 +556,7 @@ export default {
533
556
  'voltage_',
534
557
  'water',
535
558
  'white',
559
+ 'wind_speed',
536
560
  'workMode',
537
561
  'workState',
538
562
  'zyx_mode',
@@ -631,8 +655,10 @@ export default {
631
655
  275: 2, // MINI-2GS (2-channel switch)
632
656
  276: 1, // S61STPF (Smart plug with power monitoring, cloud-only)
633
657
  277: 1, // MINI-DIM (Dimmer with power monitoring)
658
+ 278: 2, // NSPanel86PB relay (2-channel switch)
659
+ 283: 1, // MINI-1GSP (Single channel switch with power monitoring)
634
660
  262: 4, // CK-BL602-SWP1-02 (4-channel switch with power monitoring)
635
- 266: 1, // SAWF-08P Air Quality Sensor (CO2, Temperature, Humidity)
661
+ 266: 1, // SAWF-08P and SAWF-07P Air Quality Sensors (CO2, Temperature, Humidity, and PM on the 07P)
636
662
  1000: 1, // "zigbee_ON_OFF_SWITCH_1000" (button device)
637
663
  1009: 1, // "" (Some sort of single switch device)
638
664
  1256: 1, // "ZIGBEE_SINGLE_SWITCH"
@@ -673,8 +699,14 @@ export default {
673
699
  7038: 1, // SONOFF AirGuard TH Zigbee temperature and humidity sensor (#767)
674
700
  7040: 2, // SONOFF MINI DUO 2-gang Zigbee switch (#768)
675
701
  7047: 1, // SONOFF Zigbee Irrigation Valve (#778)
702
+ 7048: 1, // SWV-ZF2E two-zone Zigbee irrigation valve
703
+ 7049: 1, // TRVZB gen 2 Zigbee thermostatic radiator valve
704
+ 7051: 1, // SNZB-06P24 Zigbee presence sensor with light level
705
+ 7052: 1, // SNZB-02B Zigbee temperature and humidity sensor
706
+ 7054: 1, // MINI-ZB1GP Zigbee switch with power monitoring
676
707
  7055: 1, // SNZB-03PR2 Zigbee presence sensor with light level
677
708
  7056: 1, // SNZB-09P Zigbee indoor siren
709
+ 7057: 1, // SNZB-02M Zigbee temperature and humidity sensor
678
710
  20001: 1, // iHost sub-device single switch
679
711
  20004: 4, // iHost sub-device 4-channel switch
680
712
  20006: 1, // single-colour dimmable LED controller (#780)
@@ -33,6 +33,8 @@ export default {
33
33
  curHumid: 'current humidifying',
34
34
  curLight: 'current light',
35
35
  curMode: 'current mode',
36
+ curPM10: 'current PM10',
37
+ curPM25: 'current PM2.5',
36
38
  curPos: 'current position',
37
39
  curPower: 'current power',
38
40
  curPur: 'current purifying',
@@ -32,6 +32,8 @@ export default {
32
32
  curHumid: 'humidification actuelle',
33
33
  curLight: 'lumière actuelle',
34
34
  curMode: 'mode actuel',
35
+ curPM10: 'PM10 actuel',
36
+ curPM25: 'PM2.5 actuel',
35
37
  curPos: 'position actuelle',
36
38
  curPower: 'alimentation actuelle',
37
39
  curPur: 'purification actuelle',
@@ -32,6 +32,8 @@ export default {
32
32
  curHumid: 'ค่าความชื้นในตอนนี้',
33
33
  curLight: 'แสงตอนนี้',
34
34
  curMode: 'โหมดตอนนี้',
35
+ curPM10: 'PM10 ตอนนี้',
36
+ curPM25: 'PM2.5 ตอนนี้',
35
37
  curPos: 'ตำแหน่งตอนนี้',
36
38
  curPower: 'กำลังไฟตอนนี้',
37
39
  curPur: 'การทำให้บริสุทธิ์ในตอนนี้',
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "displayName": "Homebridge eWeLink",
4
4
  "alias": "eWeLink",
5
5
  "type": "module",
6
- "version": "12.19.1-beta.4",
6
+ "version": "12.19.1-beta.5",
7
7
  "description": "Homebridge plugin to integrate eWeLink devices into HomeKit.",
8
8
  "author": {
9
9
  "name": "bwp91",