@homebridge-plugins/homebridge-ewelink 12.14.1 → 12.15.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,6 +2,17 @@
2
2
 
3
3
  All notable changes to `@homebridge-plugins/homebridge-ewelink` will be documented in this file.
4
4
 
5
+ ## v12.15.0 (2026-07-12)
6
+
7
+ ### Changed
8
+
9
+ - feat: add a per-device `invertGarageState` option to swap the reported open/closed state on garage doors without a contact sensor (#787)
10
+ - feat: add support for UIID 20006 dimmable LED controller (#780), the SONOFF AirGuard TH sensor (7038) (#767) and the SONOFF MINI DUO 2-gang switch (7040) (#768)
11
+ - chore: update `actions/checkout` workflow action to v7
12
+ - chore: remove stray `exit 1` from the deprecation workflow script
13
+ - chore: dependency updates
14
+ - docs: note homebridge v2 support in the prerequisites
15
+
5
16
  ## v12.14.1 (2026-06-03)
6
17
 
7
18
  ### Changed
package/README.md CHANGED
@@ -30,7 +30,7 @@ Homebridge plugin to integrate eWeLink devices into HomeKit
30
30
 
31
31
  - To use this plugin, you will need to already have:
32
32
  - [Node](https://nodejs.org): latest version of `v22` or `v24` - any other major version is not supported.
33
- - [Homebridge](https://homebridge.io): `v1.6` - refer to link for more information and installation instructions.
33
+ - [Homebridge](https://homebridge.io): `v1.6` or above - refer to link for more information and installation instructions.
34
34
 
35
35
  ### Setup
36
36
 
@@ -423,6 +423,14 @@
423
423
  "functionBody": "return (model.singleDevices && model.singleDevices[arrayIndices] && ['garage'].includes(model.singleDevices[arrayIndices].showAs) && !model.singleDevices[arrayIndices].ignoreDevice);"
424
424
  }
425
425
  },
426
+ "invertGarageState": {
427
+ "type": "boolean",
428
+ "title": "Invert Garage State",
429
+ "description": "Swap the reported open and closed states for this garage door. Useful if HomeKit shows the door as open when it is physically closed (and vice versa). This applies to garage doors without a defined contact sensor.",
430
+ "condition": {
431
+ "functionBody": "return (model.singleDevices && model.singleDevices[arrayIndices] && ['garage'].includes(model.singleDevices[arrayIndices].showAs) && !model.singleDevices[arrayIndices].sensorId && !model.singleDevices[arrayIndices].ignoreDevice);"
432
+ }
433
+ },
426
434
  "sensorId": {
427
435
  "type": "string",
428
436
  "title": "Sensor",
package/eslint.config.js CHANGED
@@ -35,7 +35,6 @@ export default antfu(
35
35
  'style/brace-style': ['error', '1tbs', { allowSingleLine: true }],
36
36
  'style/quote-props': ['error', 'consistent-as-needed'],
37
37
  'test/no-only-tests': 'error',
38
- 'unicorn/no-useless-spread': 'error',
39
38
  'unused-imports/no-unused-vars': ['error', { caughtErrors: 'none' }],
40
39
  },
41
40
  typescript: false,
@@ -25,6 +25,11 @@ export default class {
25
25
  this.operationTimeUp = deviceConf.operationTime || platformConsts.defaultValues.operationTime
26
26
  this.operationTimeDown = deviceConf.operationTimeDown || this.operationTimeUp
27
27
 
28
+ // Some users' garage door reports the opposite state to reality (e.g. a
29
+ // MINI-D wired to the NO contact). This option swaps the reported open and
30
+ // closed states for this device.
31
+ this.invertGarage = deviceConf.invertGarageState === true
32
+
28
33
  // Set the correct logging variables for this accessory
29
34
  switch (deviceConf.overrideLogging) {
30
35
  case 'standard':
@@ -64,8 +69,8 @@ export default class {
64
69
  this.service = this.accessory.getService(this.hapServ.GarageDoorOpener)
65
70
  if (!this.service) {
66
71
  this.service = this.accessory.addService(this.hapServ.GarageDoorOpener)
67
- this.service.updateCharacteristic(this.hapChar.CurrentDoorState, 1)
68
- this.service.updateCharacteristic(this.hapChar.TargetDoorState, 1)
72
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, this.invPos(1))
73
+ this.service.updateCharacteristic(this.hapChar.TargetDoorState, this.invPos(1))
69
74
  this.service.updateCharacteristic(this.hapChar.ObstructionDetected, false)
70
75
  this.service.addCharacteristic(this.eveChar.LastActivation)
71
76
  this.service.addCharacteristic(this.eveChar.ResetTotal)
@@ -134,8 +139,9 @@ export default class {
134
139
 
135
140
  // Add the set handler to the target position characteristic
136
141
  this.service.getCharacteristic(this.hapChar.TargetDoorState).onSet((value) => {
137
- // We don't use await as we want the callback to be run straight away
138
- this.internalUpdate(value)
142
+ // We don't use await as we want the callback to be run straight away.
143
+ // Map the HomeKit target to the internal logical position first.
144
+ this.internalUpdate(this.invPos(value))
139
145
  })
140
146
 
141
147
  // Add the get handlers only if the user hasn't disabled the disableNoResponse setting
@@ -184,27 +190,49 @@ export default class {
184
190
  this.log('[%s] %s %s.', this.name, this.lang.devInitOpts, opts)
185
191
  }
186
192
 
193
+ // Swap Open<->Closed and Opening<->Closing when this device is inverted.
194
+ // This is an involution, so the same function maps a HomeKit-facing value to
195
+ // the internal logical value and back. A no-op when inversion is disabled, or
196
+ // when a contact sensor is defined (the sensor already reports the real state).
197
+ invPos(pos) {
198
+ if (!this.invertGarage || this.definedSensor) {
199
+ return pos
200
+ }
201
+ switch (pos) {
202
+ case 0:
203
+ return 1
204
+ case 1:
205
+ return 0
206
+ case 2:
207
+ return 3
208
+ case 3:
209
+ return 2
210
+ default:
211
+ return pos
212
+ }
213
+ }
214
+
187
215
  async internalUpdate(value) {
188
216
  try {
189
217
  const newPos = value
190
218
  const params = {}
191
219
  let delay = 0
192
- const prevState = this.service.getCharacteristic(this.hapChar.CurrentDoorState).value
220
+ const prevState = this.invPos(this.service.getCharacteristic(this.hapChar.CurrentDoorState).value)
193
221
  if (newPos === prevState % 2) {
194
222
  return
195
223
  }
196
224
  this.inUse = true
197
225
  this.cacheState = value
198
226
  if (this.setup === 'switchSingle' && [2, 3].includes(prevState)) {
199
- this.service.updateCharacteristic(this.hapChar.CurrentDoorState, ((prevState * 2) % 3) + 2)
227
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, this.invPos(((prevState * 2) % 3) + 2))
200
228
  delay = 1500
201
229
  }
202
230
  if (this.cacheState !== newPos) {
203
231
  return
204
232
  }
205
233
  await sleep(delay)
206
- this.service.updateCharacteristic(this.hapChar.TargetDoorState, newPos)
207
- this.service.updateCharacteristic(this.hapChar.CurrentDoorState, newPos + 2)
234
+ this.service.updateCharacteristic(this.hapChar.TargetDoorState, this.invPos(newPos))
235
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, this.invPos(newPos + 2))
208
236
  // SCM (single-channel multi-format) devices use the switches[] array
209
237
  // but only expose outlet 0. True dual-channel garage controllers
210
238
  // (e.g. DUALR3) use separate outlets for open/close relay action.
@@ -254,7 +282,7 @@ export default class {
254
282
  const operationTime = newPos === 0 ? this.operationTimeUp : this.operationTimeDown
255
283
  await sleep(Math.max((operationTime - 20) * 100, 0))
256
284
  if (!this.definedSensor) {
257
- this.service.updateCharacteristic(this.hapChar.CurrentDoorState, newPos)
285
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, this.invPos(newPos))
258
286
  if (newPos === 1) {
259
287
  this.accessory.eveService.addEntry({ status: 1 })
260
288
  }
@@ -263,7 +291,7 @@ export default class {
263
291
  '[%s] %s [%s].',
264
292
  this.name,
265
293
  this.lang.curState,
266
- newPos === 0 ? this.lang.doorOpen : this.lang.doorClosed,
294
+ this.invPos(newPos) === 0 ? this.lang.doorOpen : this.lang.doorClosed,
267
295
  )
268
296
  }
269
297
  }
@@ -344,7 +372,7 @@ export default class {
344
372
  }
345
373
 
346
374
  if (!this.inUse && !this.definedSensor) {
347
- const prevState = this.service.getCharacteristic(this.hapChar.CurrentDoorState).value
375
+ const prevState = this.invPos(this.service.getCharacteristic(this.hapChar.CurrentDoorState).value)
348
376
  const newPos = [0, 2].includes(prevState) ? 3 : 2
349
377
  if (this.setup === 'switchMulti' && params.switches?.length >= 2) {
350
378
  if (
@@ -361,8 +389,8 @@ export default class {
361
389
  return
362
390
  }
363
391
  this.inUse = true
364
- this.service.updateCharacteristic(this.hapChar.TargetDoorState, newPos - 2)
365
- this.service.updateCharacteristic(this.hapChar.CurrentDoorState, newPos)
392
+ this.service.updateCharacteristic(this.hapChar.TargetDoorState, this.invPos(newPos - 2))
393
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, this.invPos(newPos))
366
394
  await sleep(2000)
367
395
  this.inUse = false
368
396
  if (newPos === 2) {
@@ -379,7 +407,7 @@ export default class {
379
407
  }
380
408
  const operationTime = newPos === 2 ? this.operationTimeUp : this.operationTimeDown
381
409
  await sleep(Math.max((operationTime - 20) * 100, 0))
382
- this.service.updateCharacteristic(this.hapChar.CurrentDoorState, newPos - 2)
410
+ this.service.updateCharacteristic(this.hapChar.CurrentDoorState, this.invPos(newPos - 2))
383
411
  if (newPos === 3) {
384
412
  this.accessory.eveService.addEntry({ status: 1 })
385
413
  }
package/lib/platform.js CHANGED
@@ -306,6 +306,7 @@ export default class {
306
306
  case 'hideSensor':
307
307
  case 'hideSwitch':
308
308
  case 'humidityOffsetFactor':
309
+ case 'invertGarageState':
309
310
  case 'isInched':
310
311
  case 'offlineAsOff':
311
312
  case 'offsetFactor':
@@ -95,6 +95,7 @@ export default {
95
95
  'hideSensor',
96
96
  'obstructId',
97
97
  'sensorType',
98
+ 'invertGarageState',
98
99
  'ipAddress',
99
100
  ],
100
101
  multiDevices: [
@@ -114,6 +115,7 @@ export default {
114
115
  'sensorId',
115
116
  'hideSensor',
116
117
  'obstructId',
118
+ 'invertGarageState',
117
119
  'ipAddress',
118
120
  ],
119
121
  lightDevices: [
@@ -324,12 +326,13 @@ export default {
324
326
  4256,
325
327
  7029,
326
328
  7030,
329
+ 7040,
327
330
  20004,
328
331
  ],
329
332
  switchMultiPower: [126, 165, 262],
330
333
  switchMate: [177],
331
334
  switchMan: [174],
332
- lightDimmer: [36, 44, 57, 277],
335
+ lightDimmer: [36, 44, 57, 277, 20006],
333
336
  lightRGB: [22],
334
337
  lightCCT: [52, 103],
335
338
  lightRGBCCT: [33, 59, 104, 135, 136, 137, 173],
@@ -353,7 +356,7 @@ export default {
353
356
  zbLightCCT: [1258],
354
357
  zbLightRGBCCT: [3258, 7009],
355
358
  zbMotor: [1514, 7006, 7034],
356
- zbSensorAmbient: [1770, 1771, 7014, 7033],
359
+ zbSensorAmbient: [1770, 1771, 7014, 7033, 7038],
357
360
  zbSensorMotion: [2026, 7002],
358
361
  zbSensorOccupancy: [7016],
359
362
  zbSensorContact: [3026, 7003],
@@ -575,8 +578,11 @@ export default {
575
578
  7030: 3, // ZBM5-3C-80/86 Zigbee 3-gang switch
576
579
  7032: 1, // Zigbee Power Monitoring Switch
577
580
  7033: 1, // SNZB-02WD Zigbee Temperature and Humidity Display Sensor
581
+ 7038: 1, // SONOFF AirGuard TH Zigbee temperature and humidity sensor (#767)
582
+ 7040: 2, // SONOFF MINI DUO 2-gang Zigbee switch (#768)
578
583
  20001: 1, // iHost sub-device single switch
579
584
  20004: 4, // iHost sub-device 4-channel switch
585
+ 20006: 1, // single-colour dimmable LED controller (#780)
580
586
  },
581
587
 
582
588
  cannotSupportDevices: {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "displayName": "Homebridge eWeLink",
4
4
  "alias": "eWeLink",
5
5
  "type": "module",
6
- "version": "12.14.1",
6
+ "version": "12.15.0",
7
7
  "description": "Homebridge plugin to integrate eWeLink devices into HomeKit.",
8
8
  "author": {
9
9
  "name": "bwp91",
@@ -61,14 +61,14 @@
61
61
  "lint:fix": "npm run lint -- --fix"
62
62
  },
63
63
  "dependencies": {
64
- "@homebridge/plugin-ui-utils": "^2.2.4",
65
- "axios": "^1.17.0",
64
+ "@homebridge/plugin-ui-utils": "^2.2.5",
65
+ "axios": "^1.18.1",
66
66
  "node-persist": "^4.0.4",
67
- "p-queue": "^9.3.0",
67
+ "p-queue": "^9.3.1",
68
68
  "websocket-as-promised": "^3.0.1",
69
69
  "ws": "^8.21.0"
70
70
  },
71
71
  "devDependencies": {
72
- "@antfu/eslint-config": "^9.0.0"
72
+ "@antfu/eslint-config": "^9.1.0"
73
73
  }
74
74
  }