@homebridge-plugins/homebridge-ewelink 12.18.3-beta.0 → 12.18.4-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,7 +2,15 @@
2
2
 
3
3
  All notable changes to `@homebridge-plugins/homebridge-ewelink` will be documented in this file.
4
4
 
5
- ## v12.18.3 (Pending Release)
5
+ ## v12.18.4 (Pending Release)
6
+
7
+ ### Changed
8
+
9
+ - feat(zigbee): add the snzb-03pr2 presence sensor, with occupancy and light level
10
+ - feat(zigbee): add the snzb-09p indoor siren, as a switch that sounds the alarm
11
+ - chore(deps): dependency updates
12
+
13
+ ## v12.18.3 (2026-08-11)
6
14
 
7
15
  ### Changed
8
16
 
@@ -8,6 +8,7 @@ import deviceSensorLeak from './sensor-leak.js'
8
8
  import deviceSensorMotion from './sensor-motion.js'
9
9
  import deviceSensorOccupancy from './sensor-occupancy.js'
10
10
  import deviceSensorSmoke from './sensor-smoke.js'
11
+ import deviceSiren from './siren.js'
11
12
  import deviceSwitchStateless from './switch-stateless.js'
12
13
  import deviceThermostat from './thermostat.js'
13
14
  import deviceZigbeeWaterValve from './zigbee-water-valve.js'
@@ -23,6 +24,7 @@ export default {
23
24
  deviceSensorMotion,
24
25
  deviceSensorOccupancy,
25
26
  deviceSensorSmoke,
27
+ deviceSiren,
26
28
  deviceSwitchStateless,
27
29
  deviceThermostat,
28
30
  deviceZigbeeWaterValve,
@@ -53,8 +53,9 @@ export default class {
53
53
  this.battService = this.accessory.getService(this.hapServ.Battery)
54
54
  || this.accessory.addService(this.hapServ.Battery)
55
55
 
56
- // SNZB-06P models also report a binary bright/dark light level (uiid 7016)
57
- if (accessory.context.eweUIID === 7016) {
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)) {
58
59
  this.lightService = this.accessory.getService(this.hapServ.LightSensor)
59
60
  || this.accessory.addService(this.hapServ.LightSensor)
60
61
  } else if (this.accessory.getService(this.hapServ.LightSensor)) {
@@ -90,6 +91,18 @@ export default class {
90
91
  }
91
92
  }
92
93
  }
94
+ if (this.lightService && hasProperty(params, 'illumination')) {
95
+ // Some models report a numeric light reading instead of a binary one.
96
+ // HomeKit accepts 0.0001 to 100000 lux, so keep it inside that
97
+ const newLux = Math.max(Math.min(Number(params.illumination), 100000), 0.0001)
98
+ if (Number.isFinite(newLux) && newLux !== this.cacheLux) {
99
+ this.cacheLux = newLux
100
+ this.lightService.updateCharacteristic(this.hapChar.CurrentAmbientLightLevel, this.cacheLux)
101
+ if (params.updateSource && this.enableLogging) {
102
+ this.log('[%s] %s [%s].', this.name, this.lang.curLightLevel, this.cacheLux)
103
+ }
104
+ }
105
+ }
93
106
  if (hasProperty(params, 'battery') && params.battery !== this.cacheBatt) {
94
107
  this.cacheBatt = params.battery
95
108
  this.cacheBattScaled = Math.max(Math.min(this.cacheBatt, 100), 0)
@@ -102,17 +115,17 @@ export default class {
102
115
  this.log('[%s] %s [%s%].', this.name, this.lang.curBatt, this.cacheBattScaled)
103
116
  }
104
117
  }
105
- if (
106
- hasProperty(params, 'human')
107
- && hasProperty(params, 'trigTime')
108
- && params.human !== this.cacheState
109
- ) {
118
+ if (hasProperty(params, 'human') && params.human !== this.cacheState) {
110
119
  this.cacheState = params.human
111
- const timeDiff = (Date.now() - params.trigTime) / 1000
120
+ // A report that carries a timestamp can be checked for staleness; the
121
+ // models that report live state without one are taken at face value
122
+ const isFresh = hasProperty(params, 'trigTime')
123
+ ? (Date.now() - params.trigTime) / 1000 < this.sensorTimeDifference
124
+ : true
112
125
  const occupancyDetected = !!(
113
126
  params.updateSource
114
127
  && params.human === 1
115
- && timeDiff < this.sensorTimeDifference
128
+ && isFresh
116
129
  )
117
130
  this.service.updateCharacteristic(this.hapChar.OccupancyDetected, occupancyDetected)
118
131
  if (occupancyDetected) {
@@ -0,0 +1,132 @@
1
+ import platformConsts from '../../utils/constants.js'
2
+ import { hasProperty } from '../../utils/functions.js'
3
+
4
+ // How long the HomeKit tile stays on after a trigger. The device runs its
5
+ // siren for whatever duration is configured in the eWeLink app and turns
6
+ // itself off without reporting that it has, so the tile resets itself rather
7
+ // than waiting for a state that never arrives.
8
+ const TILE_RESET_MS = 3000
9
+
10
+ export default class {
11
+ constructor(platform, accessory) {
12
+ // Set up variables from the platform
13
+ this.hapChar = platform.api.hap.Characteristic
14
+ this.hapErr = platform.api.hap.HapStatusError
15
+ this.hapServ = platform.api.hap.Service
16
+ this.lang = platform.lang
17
+ this.log = platform.log
18
+ this.platform = platform
19
+
20
+ // Set up variables from the accessory
21
+ this.name = accessory.displayName
22
+ this.accessory = accessory
23
+
24
+ // Set up custom variables for this device type
25
+ const deviceConf = platform.deviceConf[accessory.context.eweDeviceId] || {}
26
+ this.lowBattThreshold = deviceConf.lowBattThreshold
27
+ ? Math.min(deviceConf.lowBattThreshold, 100)
28
+ : platformConsts.defaultValues.lowBattThreshold
29
+
30
+ // Set the correct logging variables for this accessory
31
+ switch (deviceConf.overrideLogging) {
32
+ case 'standard':
33
+ this.enableLogging = true
34
+ this.enableDebugLogging = false
35
+ break
36
+ case 'debug':
37
+ this.enableLogging = true
38
+ this.enableDebugLogging = true
39
+ break
40
+ case 'disable':
41
+ this.enableLogging = false
42
+ this.enableDebugLogging = false
43
+ break
44
+ default:
45
+ this.enableLogging = !platform.config.disableDeviceLogging
46
+ this.enableDebugLogging = false
47
+ break
48
+ }
49
+
50
+ // Add the switch service if it doesn't already exist
51
+ this.service = this.accessory.getService(this.hapServ.Switch)
52
+ || this.accessory.addService(this.hapServ.Switch)
53
+ this.service.updateCharacteristic(this.hapChar.On, false)
54
+
55
+ // Add the battery service if it doesn't already exist
56
+ this.battService = this.accessory.getService(this.hapServ.Battery)
57
+ || this.accessory.addService(this.hapServ.Battery)
58
+
59
+ // Set up the handler for the switch
60
+ this.service.getCharacteristic(this.hapChar.On).onSet(async (value) => {
61
+ try {
62
+ if (!value) {
63
+ // The device stops on its own after its configured duration and
64
+ // offers no early-stop command, so an off is only the tile resetting
65
+ return
66
+ }
67
+
68
+ // The trigger rides on the device's own alarm settings, so send the
69
+ // last reported settings back with the trigger flag added - a bare
70
+ // flag could otherwise reset the volume and duration to defaults
71
+ const setting = { ...(this.accessory.context.cacheAlarmSetting || {}), test: true }
72
+ await this.platform.sendDeviceUpdate(this.accessory, { alarmSetting: setting })
73
+
74
+ if (this.enableLogging) {
75
+ this.log('[%s] %s [on].', this.name, this.lang.curState)
76
+ }
77
+
78
+ // Reset the tile shortly after - see TILE_RESET_MS
79
+ clearTimeout(this.resetTimer)
80
+ this.resetTimer = setTimeout(() => {
81
+ this.service.updateCharacteristic(this.hapChar.On, false)
82
+ }, TILE_RESET_MS)
83
+ } catch (err) {
84
+ this.platform.deviceUpdateError(this.accessory, err, false)
85
+ setTimeout(() => {
86
+ this.service.updateCharacteristic(this.hapChar.On, false)
87
+ }, 2000)
88
+ throw new this.hapErr(-70402)
89
+ }
90
+ })
91
+
92
+ // Output the customised options to the log
93
+ const normalLogging = this.enableLogging ? 'standard' : 'disable'
94
+ const opts = JSON.stringify({
95
+ logging: this.enableDebugLogging ? 'debug' : normalLogging,
96
+ lowBattThreshold: this.lowBattThreshold,
97
+ })
98
+ this.log('[%s] %s %s.', this.name, this.lang.devInitOpts, opts)
99
+ }
100
+
101
+ async externalUpdate(params) {
102
+ try {
103
+ // Keep the device's own alarm settings so a trigger can send them back
104
+ if (hasProperty(params, 'alarmSetting') && typeof params.alarmSetting === 'object') {
105
+ this.accessory.context.cacheAlarmSetting = { ...params.alarmSetting }
106
+ delete this.accessory.context.cacheAlarmSetting.test
107
+ }
108
+ if (hasProperty(params, 'battery') && params.battery !== this.cacheBatt) {
109
+ this.cacheBatt = params.battery
110
+ this.cacheBattScaled = Math.max(Math.min(this.cacheBatt, 100), 0)
111
+ this.battService.updateCharacteristic(this.hapChar.BatteryLevel, this.cacheBattScaled)
112
+ this.battService.updateCharacteristic(
113
+ this.hapChar.StatusLowBattery,
114
+ this.cacheBattScaled < this.lowBattThreshold ? 1 : 0,
115
+ )
116
+ if (params.updateSource && this.enableLogging) {
117
+ this.log('[%s] %s [%s%].', this.name, this.lang.curBatt, this.cacheBattScaled)
118
+ }
119
+ }
120
+ } catch (err) {
121
+ this.platform.deviceUpdateError(this.accessory, err, false)
122
+ }
123
+ }
124
+
125
+ destroy() {
126
+ clearTimeout(this.resetTimer)
127
+ }
128
+
129
+ markStatus(isOnline) {
130
+ this.isOnline = isOnline
131
+ }
132
+ }
@@ -33,6 +33,7 @@ const categoryToArray = {
33
33
  zbSensorContact: 'sensorDevices',
34
34
  zbSensorWater: 'sensorDevices',
35
35
  zbSensorSmoke: 'sensorDevices',
36
+ zbSiren: 'switchDevices',
36
37
  }
37
38
 
38
39
  class PluginUiServer extends HomebridgePluginUiServer {
package/lib/platform.js CHANGED
@@ -1729,6 +1729,16 @@ export default class {
1729
1729
  this.applyAccessoryLogging(accessory)
1730
1730
  accessory.control = new deviceTypes.zb.deviceSensorSmoke(this, accessory)
1731
1731
  /** */
1732
+ } else if (platformConsts.devices.zbSiren.includes(uiid)) {
1733
+ /**
1734
+ **************
1735
+ ZB SIREN
1736
+ ***************
1737
+ */
1738
+ accessory = devicesInHB.get(uuid) || this.addAccessory(device, `${device.deviceid}SWX`)
1739
+ this.applyAccessoryLogging(accessory)
1740
+ accessory.control = new deviceTypes.zb.deviceSiren(this, accessory)
1741
+ /** */
1732
1742
  } else if (platformConsts.devices.zbThermostat.includes(uiid)) {
1733
1743
  /**
1734
1744
  *******************
@@ -377,10 +377,11 @@ export default {
377
377
  zbMotor: [1514, 7006, 7034],
378
378
  zbSensorAmbient: [242, 1770, 1771, 7014, 7033, 7038],
379
379
  zbSensorMotion: [2026, 7002],
380
- zbSensorOccupancy: [7016],
380
+ zbSensorOccupancy: [7016, 7055],
381
381
  zbSensorContact: [3026, 7003],
382
382
  zbSensorWater: [4026, 7019],
383
383
  zbSensorSmoke: [5026],
384
+ zbSiren: [7056],
384
385
  zbThermostat: [7017],
385
386
  zbWaterValve: [7027, 7047],
386
387
  group: [5000],
@@ -604,6 +605,8 @@ export default {
604
605
  7038: 1, // SONOFF AirGuard TH Zigbee temperature and humidity sensor (#767)
605
606
  7040: 2, // SONOFF MINI DUO 2-gang Zigbee switch (#768)
606
607
  7047: 1, // SONOFF Zigbee Irrigation Valve (#778)
608
+ 7055: 1, // SNZB-03PR2 Zigbee presence sensor with light level
609
+ 7056: 1, // SNZB-09P Zigbee indoor siren
607
610
  20001: 1, // iHost sub-device single switch
608
611
  20004: 4, // iHost sub-device 4-channel switch
609
612
  20006: 1, // single-colour dimmable LED controller (#780)
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "displayName": "Homebridge eWeLink",
4
4
  "alias": "eWeLink",
5
5
  "type": "module",
6
- "version": "12.18.3-beta.0",
6
+ "version": "12.18.4-beta.0",
7
7
  "description": "Homebridge plugin to integrate eWeLink devices into HomeKit.",
8
8
  "author": {
9
9
  "name": "bwp91",
@@ -66,8 +66,8 @@
66
66
  "changelog:sync": "node .github/scripts/sync-changelog.mjs"
67
67
  },
68
68
  "dependencies": {
69
- "@homebridge/plugin-ui-utils": "^2.2.5",
70
- "axios": "^1.19.0",
69
+ "@homebridge/plugin-ui-utils": "^2.2.6",
70
+ "axios": "^1.20.0",
71
71
  "node-persist": "^4.0.4",
72
72
  "p-queue": "^9.3.3",
73
73
  "websocket-as-promised": "^3.0.1",
@@ -75,7 +75,7 @@
75
75
  },
76
76
  "devDependencies": {
77
77
  "@antfu/eslint-config": "^9.3.0",
78
- "@vitest/coverage-v8": "^4.1.10",
79
- "vitest": "^4.1.10"
78
+ "@vitest/coverage-v8": "^4.1.11",
79
+ "vitest": "^4.1.11"
80
80
  }
81
81
  }