homebridge-deconz 1.0.13 → 1.0.17

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.
@@ -61,6 +61,11 @@ class Device {
61
61
  * @type {boolean}
62
62
  */
63
63
  this.zigbee = resource.zigbee
64
+
65
+ /** Device has a resource with `config.battery` in their `body`.
66
+ * @type {boolean}
67
+ */
68
+ this.hasBattery = resource.body.config?.battery !== undefined
64
69
  }
65
70
 
66
71
  /** The delegate of the primary resource of the device.
@@ -105,6 +110,9 @@ class Device {
105
110
  this.subtypesByServiceName[resource.serviceName].push(resource.subtype)
106
111
  }
107
112
  this.resourceBySubtype[subtype] = resource
113
+ if (resource.body.config?.battery !== undefined) {
114
+ this.hasBattery = true
115
+ }
108
116
  const p = this.resourceBySubtype[this.primary]
109
117
  if (p.rtype === rtype && p.prio < prio) {
110
118
  this.primary = resource.subtype
@@ -112,6 +112,9 @@ class Resource {
112
112
  */
113
113
  this.body = toObject('body', body)
114
114
  toString('body.name', body.name, true)
115
+ body.name = body.name.replace(/[^\p{L}\p{N} ']/ug, ' ')
116
+ .replace(/^[ ']*/, '')
117
+ .replace(/[ ']*$/, '')
115
118
  toString('body.type', body.type, true)
116
119
 
117
120
  let realDevice = false
@@ -132,6 +135,9 @@ class Resource {
132
135
  * @type {string}
133
136
  */
134
137
  this.id = mac
138
+ if (gateway.splitdevice[this.rtype]?.[this.rid]) {
139
+ this.id += '-' + endpoint
140
+ }
135
141
 
136
142
  /** The subtype of the corresponding HomeKit service.
137
143
  *
@@ -302,6 +308,7 @@ class Resource {
302
308
  case 'Consumption': return 'Consumption'
303
309
  // case 'DoorLock': return null
304
310
  case 'Daylight': return 'Daylight'
311
+ case 'DaylightOffset': return ''
305
312
  case 'Fire': return 'Smoke'
306
313
  case 'GenericFlag': return 'Flag'
307
314
  case 'GenericStatus': return 'Status'
@@ -536,7 +543,7 @@ class Resource {
536
543
  buttons.push([2, 'Close', SINGLE | LONG])
537
544
  break
538
545
  case 'TRADFRI remote control':
539
- buttons.push([1, 'On/Off', SINGLE])
546
+ buttons.push([1, 'Power', SINGLE])
540
547
  buttons.push([2, 'Dim Up', SINGLE | LONG])
541
548
  buttons.push([3, 'Dim Down', SINGLE | LONG])
542
549
  buttons.push([4, 'Previous', SINGLE | LONG])
@@ -580,7 +587,7 @@ class Resource {
580
587
  case 'LDS':
581
588
  switch (this.model) {
582
589
  case 'ZBT-DIMController-D0800':
583
- buttons.push([1, 'On/Off', SINGLE])
590
+ buttons.push([1, 'Power', SINGLE])
584
591
  buttons.push([2, 'Dim Up', SINGLE | LONG])
585
592
  buttons.push([3, 'Dim Down', SINGLE | LONG])
586
593
  buttons.push([4, 'Scene', SINGLE | LONG])
@@ -719,7 +726,7 @@ class Resource {
719
726
  case 'MLI':
720
727
  switch (this.model) {
721
728
  case 'ZBT-Remote-ALL-RGBW': // Tint remote control by Müller-Licht see deconz-rest-plugin#1209
722
- buttons.push([1, 'On/Off', SINGLE])
729
+ buttons.push([1, 'Power', SINGLE])
723
730
  buttons.push([2, 'Dim Up', SINGLE | LONG])
724
731
  buttons.push([3, 'Dim Down', SINGLE | LONG])
725
732
  buttons.push([4, 'Warm', SINGLE])
@@ -802,7 +809,7 @@ class Resource {
802
809
  buttons.push([4, 'Off', SINGLE | LONG])
803
810
  break
804
811
  case 'RWL022': // Hue dimmer switch (2021)
805
- buttons.push([1, 'On/Off', SINGLE | LONG])
812
+ buttons.push([1, 'Power', SINGLE | LONG])
806
813
  buttons.push([2, 'Dim Up', SINGLE | LONG, true])
807
814
  buttons.push([3, 'Dim Down', SINGLE | LONG, true])
808
815
  buttons.push([4, 'Hue', SINGLE | LONG])
@@ -1055,7 +1062,7 @@ class Resource {
1055
1062
  switch (this.model) {
1056
1063
  case 'RC 110':
1057
1064
  if (this.endpoint === '01') {
1058
- buttons.push([1, 'On/Off', SINGLE])
1065
+ buttons.push([1, 'Power', SINGLE])
1059
1066
  buttons.push([2, 'Dim Up', SINGLE | LONG])
1060
1067
  buttons.push([3, 'Dim Down', SINGLE | LONG])
1061
1068
  buttons.push([4, '1', SINGLE])
@@ -1066,7 +1073,7 @@ class Resource {
1066
1073
  buttons.push([9, '6', SINGLE])
1067
1074
  for (let i = 1; i <= 6; i++) {
1068
1075
  const button = 7 + i * 3
1069
- buttons.push([button, `On/Off ${i}`, SINGLE])
1076
+ buttons.push([button, `Power ${i}`, SINGLE])
1070
1077
  buttons.push([button + 1, `Dim Up ${i}`, SINGLE | LONG])
1071
1078
  buttons.push([button + 2, `Dim Down ${i}`, SINGLE | LONG])
1072
1079
  }
@@ -16,14 +16,10 @@ import '../Deconz/Resource.js'
16
16
  import '../Deconz/Device.js'
17
17
 
18
18
  import { DeconzAccessory } from '../DeconzAccessory/index.js'
19
- import '../DeconzAccessory/AirPurifier.js'
20
- import '../DeconzAccessory/Light.js'
21
- import '../DeconzAccessory/Sensor.js'
22
- import '../DeconzAccessory/Thermostat.js'
23
- import '../DeconzAccessory/WarningDevice.js'
24
- import '../DeconzAccessory/WindowCovering.js'
25
19
 
26
20
  import { DeconzService } from '../DeconzService/index.js'
21
+ import '../DeconzService/Button.js'
22
+ import '../DeconzService/Gateway.js'
27
23
 
28
24
  const { HttpError } = ApiClient
29
25
 
@@ -153,7 +149,7 @@ class Gateway extends AccessoryDelegate {
153
149
 
154
150
  this.addPropertyDelegate({
155
151
  key: 'periodicEvents',
156
- value: true,
152
+ value: false,
157
153
  silent: true
158
154
  })
159
155
 
@@ -571,7 +567,6 @@ class Gateway extends AccessoryDelegate {
571
567
  this.exposeErrors = {}
572
568
  this.context.settingsById = {}
573
569
  this.context.fullState = null
574
- this.values.logLevel = 2
575
570
  } catch (error) { this.error(error) }
576
571
  }
577
572
 
@@ -615,11 +610,47 @@ class Gateway extends AccessoryDelegate {
615
610
  this.pollNext = true
616
611
  }
617
612
 
613
+ /** On-demand import of a DeconzAccessory subclass.
614
+ * @params {string} type - The name of the class.
615
+ */
616
+ async importAccessoryType (type) {
617
+ switch (type) {
618
+ case 'AirPurifier':
619
+ case 'Light':
620
+ case 'Sensor':
621
+ case 'Thermostat':
622
+ case 'WarningDevice':
623
+ case 'WindowCovering':
624
+ break
625
+ case 'Outlet':
626
+ case 'Switch':
627
+ type = 'Light'
628
+ break
629
+ default:
630
+ type = 'Sensor'
631
+ break
632
+ }
633
+ if (DeconzAccessory[type] == null) {
634
+ this.vdebug('importing DeconzAccessory.%s', type)
635
+ await import('../DeconzAccessory/' + type + '.js')
636
+ }
637
+ }
638
+
639
+ /** On-demand import of a a DeconzService subclass.
640
+ * @params {string} type - The name of the class.
641
+ */
642
+ async importServiceType (type) {
643
+ if (DeconzService[type] == null) {
644
+ this.vdebug('importing DeconzService.%s', type)
645
+ await import('../DeconzService/' + type + '.js')
646
+ }
647
+ }
648
+
618
649
  /** Add the accessory for the device.
619
650
  * @params {string} id - The device ID.
620
651
  * @return {?DeconzAccessory} - The accessory delegate.
621
652
  */
622
- addAccessory (id) {
653
+ async addAccessory (id) {
623
654
  if (id === this.id) {
624
655
  throw new RangeError(`${id}: gateway ID`)
625
656
  }
@@ -632,10 +663,17 @@ class Gateway extends AccessoryDelegate {
632
663
  const { body } = device.resource
633
664
  this.log('%s: add accessory', body.name)
634
665
  let { serviceName } = device.resource
666
+ await this.importAccessoryType(serviceName)
635
667
  if (DeconzAccessory[serviceName] == null) {
636
668
  // this.warn('%s: %s: not yet supported %s type', body.name, body.type, rtype)
637
669
  serviceName = 'Sensor'
638
670
  }
671
+ for (const resourceServiceName of Object.keys(device.subtypesByServiceName)) {
672
+ await this.importServiceType(resourceServiceName)
673
+ }
674
+ if (device.hasBattery) {
675
+ await this.importServiceType('Battery')
676
+ }
639
677
  const accessory = new DeconzAccessory[serviceName](this, device)
640
678
  this.accessoryById[id] = accessory
641
679
  this.monitorResources(accessory, true)
@@ -969,6 +1007,7 @@ class Gateway extends AccessoryDelegate {
969
1007
  this.context.fullState.config = config
970
1008
  this.context.fullState.lights = await this.client.get('/lights')
971
1009
  this.context.fullState.sensors = await this.client.get('/sensors')
1010
+ this.context.fullState.resourcelinks = await this.client.get('/resourcelinks')
972
1011
  if (this.nDevicesByRtype.groups > 0) {
973
1012
  this.context.fullState.groups = await this.client.get('/groups')
974
1013
  this.context.fullState.groups[0] = await this.client.get('/groups/0')
@@ -992,6 +1031,54 @@ class Gateway extends AccessoryDelegate {
992
1031
  }
993
1032
  }
994
1033
 
1034
+ /* Analyse blacklist resourcelinks.
1035
+ */
1036
+ analyseResourcelinks (logUnsupported = false) {
1037
+ const warn = (logUnsupported ? this.warn : this.vdebug).bind(this)
1038
+ /** Blacklisted resources.
1039
+ *
1040
+ * Updated by
1041
+ * {@link DeconzAccessory.Gateway#analyseBlacklist analyseBlacklist()}.
1042
+ * @type {Object<string, boolean>}
1043
+ */
1044
+ this.blacklist = {
1045
+ lights: {},
1046
+ sensors: {}
1047
+ }
1048
+ this.splitdevice = {
1049
+ lights: {},
1050
+ sensors: {}
1051
+ }
1052
+ for (const key in this.context.fullState.resourcelinks) {
1053
+ const link = this.context.fullState.resourcelinks[key]
1054
+ if (
1055
+ link.name === 'homebridge-deconz' && link.links != null &&
1056
+ link.description != null
1057
+ ) {
1058
+ const type = link.description.toLowerCase()
1059
+ switch (type) {
1060
+ case 'migration':
1061
+ break
1062
+ case 'splitdevice':
1063
+ case 'blacklist':
1064
+ this.debug('/resourcelinks/%d: %d %s entries', key, link.links.length, type)
1065
+ for (const resource of link.links) {
1066
+ const rtype = resource.split('/')[1]
1067
+ const rid = resource.split('/')[2]
1068
+ if (this[type][rtype] == null) {
1069
+ warn('/resourcelinks/%d: %s: ignoring unsupported %s resource', key, resource, type)
1070
+ continue
1071
+ }
1072
+ this[type][rtype][rid] = true
1073
+ }
1074
+ break
1075
+ default:
1076
+ warn('/resourcelinks/%d: %s: ignoring unsupported resourcelink', key, type)
1077
+ }
1078
+ }
1079
+ }
1080
+ }
1081
+
995
1082
  /** Analyse the peristed full state of the gateway,
996
1083
  * adding, re-configuring, and deleting delegates for corresponding HomeKit
997
1084
  * accessories and services.
@@ -1056,6 +1143,7 @@ class Gateway extends AccessoryDelegate {
1056
1143
  this.nDevicesByRtype = {}
1057
1144
 
1058
1145
  this.vdebug('analysing resources...')
1146
+ this.analyseResourcelinks(params.logUnsupported)
1059
1147
  for (const rtype of rtypes) {
1060
1148
  this.deviceByRidByRtype[rtype] = {}
1061
1149
  for (const rid in fullState[rtype]) {
@@ -1213,10 +1301,16 @@ class Gateway extends AccessoryDelegate {
1213
1301
  * unsupported resources.
1214
1302
  */
1215
1303
  analyseResource (rtype, rid, body, logUnsupported) {
1304
+ const warn = (logUnsupported ? this.warn : this.vdebug).bind(this)
1305
+ const debug = (logUnsupported ? this.debug : this.vdebug).bind(this)
1306
+
1216
1307
  const resource = new Deconz.Resource(this, rtype, rid, body)
1217
1308
  const { id, serviceName } = resource
1309
+ if (this.blacklist[rtype]?.[rid]) {
1310
+ debug('%s: /%s/%d: ignoring blacklisted resource', id, rtype, rid)
1311
+ return
1312
+ }
1218
1313
  if (id === this.id || serviceName === '') {
1219
- const debug = (logUnsupported ? this.debug : this.vdebug).bind(this)
1220
1314
  debug(
1221
1315
  '%s: /%s/%d: %s: ignoring unsupported %s type',
1222
1316
  id, rtype, rid, body.type, rtype
@@ -1224,7 +1318,6 @@ class Gateway extends AccessoryDelegate {
1224
1318
  return
1225
1319
  }
1226
1320
  if (serviceName == null) {
1227
- const warn = (logUnsupported ? this.warn : this.vdebug).bind(this)
1228
1321
  warn(
1229
1322
  '%s: /%s/%d: %s: ignoring unknown %s type',
1230
1323
  id, rtype, rid, body.type, rtype
@@ -9,35 +9,7 @@ import { OptionParser } from 'homebridge-lib/OptionParser'
9
9
  import { ApiClient } from 'hb-deconz-tools/ApiClient'
10
10
 
11
11
  import { DeconzService } from '../DeconzService/index.js'
12
- import '../DeconzService/AirPressure.js'
13
- import '../DeconzService/AirPurifier.js'
14
- import '../DeconzService/AirQuality.js'
15
- import '../DeconzService/Alarm.js'
16
- import '../DeconzService/Battery.js'
17
12
  import '../DeconzService/Button.js'
18
- import '../DeconzService/CarbonMonoxide.js'
19
- import '../DeconzService/Consumption.js'
20
- import '../DeconzService/Contact.js'
21
- import '../DeconzService/Daylight.js'
22
- import '../DeconzService/Flag.js'
23
- import '../DeconzService/Gateway.js'
24
- import '../DeconzService/Humidity.js'
25
- import '../DeconzService/Label.js'
26
- import '../DeconzService/Leak.js'
27
- import '../DeconzService/Light.js'
28
- import '../DeconzService/LightLevel.js'
29
- import '../DeconzService/Motion.js'
30
- import '../DeconzService/Outlet.js'
31
- import '../DeconzService/Power.js'
32
- import '../DeconzService/Schedule.js'
33
- import '../DeconzService/Status.js'
34
- import '../DeconzService/Smoke.js'
35
- import '../DeconzService/Switch.js'
36
- import '../DeconzService/Temperature.js'
37
- import '../DeconzService/Thermostat.js'
38
- import '../DeconzService/Valve.js'
39
- import '../DeconzService/WarningDevice.js'
40
- import '../DeconzService/WindowCovering.js'
41
13
 
42
14
  const { HttpError } = ApiClient
43
15
  const { SINGLE, DOUBLE, LONG } = DeconzService.Button
@@ -264,7 +236,6 @@ class DeconzAccessory extends AccessoryDelegate {
264
236
  lowBatteryThreshold: this.servicesByServiceName?.Battery?.[0].values.lowBatteryThreshold,
265
237
  // offset: this.servicesByServiceName?.Temperature?.[0].values.offset,
266
238
  serviceName: this.values.serviceName,
267
- splitLight: undefined,
268
239
  venetianBlind: this.service.values.venetianBlind,
269
240
  useExternalTemperature: this.service.values.useExternalTemperature,
270
241
  wallSwitch: this.service.values.wallSwitch
@@ -15,7 +15,6 @@ class LightsResource extends DeconzService {
15
15
 
16
16
  this.updating = 0
17
17
  this.targetState = {}
18
- this.deferrals = []
19
18
  }
20
19
 
21
20
  addCharacteristicDelegates (params = {}) {
@@ -23,6 +23,15 @@ class Motion extends DeconzService.SensorsResource {
23
23
  value: false
24
24
  })
25
25
 
26
+ if (resource.body.state.distance !== undefined) {
27
+ this.addCharacteristicDelegate({
28
+ key: 'distance',
29
+ Characteristic: this.Characteristics.my.Distance,
30
+ unit: ' cm',
31
+ value: 0
32
+ })
33
+ }
34
+
26
35
  this.addCharacteristicDelegate({
27
36
  key: 'sensitivity',
28
37
  Characteristic: this.Characteristics.eve.Sensitivity
@@ -50,6 +59,32 @@ class Motion extends DeconzService.SensorsResource {
50
59
  }
51
60
  })
52
61
 
62
+ if (resource.body.config.detectionrange !== undefined) {
63
+ this.addCharacteristicDelegate({
64
+ key: 'detectionRange',
65
+ Characteristic: this.Characteristics.my.DetectionRange,
66
+ unit: ' cm',
67
+ value: 600
68
+ }).on('didSet', async (value, fromHomeKit) => {
69
+ if (fromHomeKit) {
70
+ const detectionrange = Math.max(0, Math.min(value, 600))
71
+ await this.putConfig({ detectionrange })
72
+ }
73
+ })
74
+ }
75
+
76
+ if (resource.body.config.resetpresence !== undefined) {
77
+ this.addCharacteristicDelegate({
78
+ key: 'reset',
79
+ Characteristic: this.Characteristics.my.Reset,
80
+ value: false
81
+ }).on('didSet', async (value, fromHomeKit) => {
82
+ if (fromHomeKit) {
83
+ await this.put('/config', { resetpresence: value })
84
+ }
85
+ })
86
+ }
87
+
53
88
  this.addCharacteristicDelegates()
54
89
 
55
90
  this.update(resource.body, resource.rpath)
@@ -59,6 +94,9 @@ class Motion extends DeconzService.SensorsResource {
59
94
  if (state.presence != null) {
60
95
  this.values.motion = state.presence
61
96
  }
97
+ if (state.distance != null) {
98
+ this.values.distance = state.distance
99
+ }
62
100
  if (state.vibration != null) {
63
101
  this.values.motion = state.vibration
64
102
  }
@@ -83,6 +121,12 @@ class Motion extends DeconzService.SensorsResource {
83
121
  ? this.Characteristics.eve.Sensitivity.LOW
84
122
  : this.Characteristics.eve.Sensitivity.MEDIUM
85
123
  }
124
+ if (config.detectionrange != null) {
125
+ this.values.detectionRange = config.detectionrange
126
+ }
127
+ if (config.resetpresence != null) {
128
+ this.values.reset = config.resetpresence
129
+ }
86
130
  super.updateConfig(config)
87
131
  }
88
132
  }
@@ -3,6 +3,8 @@
3
3
  //
4
4
  // Homebridge plugin for deCONZ.
5
5
 
6
+ import { timeout } from 'homebridge-lib'
7
+
6
8
  import { ApiClient } from 'hb-deconz-tools/ApiClient'
7
9
 
8
10
  import { DeconzService } from '../DeconzService/index.js'
@@ -13,9 +15,12 @@ const { dateToString } = ApiClient
13
15
  * @memberof DeconzService
14
16
  */
15
17
  class SensorsResource extends DeconzService {
16
- // constructor (accessory, resource, params) {
17
- // super(accessory, resource, params)
18
- // }
18
+ constructor (accessory, resource, params) {
19
+ super(accessory, resource, params)
20
+
21
+ this.updating = 0
22
+ this.targetConfig = {}
23
+ }
19
24
 
20
25
  addCharacteristicDelegates (params = {}) {
21
26
  if (!params.noLastUpdated) {
@@ -83,6 +88,34 @@ class SensorsResource extends DeconzService {
83
88
  return this.put('/config', { alert: 'select' })
84
89
  }
85
90
  }
91
+
92
+ // Collect config changes into a combined request.
93
+ async putConfig (config) {
94
+ for (const key in config) {
95
+ this.resource.body.config[key] = config[key]
96
+ this.targetConfig[key] = config[key]
97
+ }
98
+ return this._putConfig()
99
+ }
100
+
101
+ // Send the request (for the combined state changes) to the gateway.
102
+ async _putConfig () {
103
+ try {
104
+ if (this.platform.config.waitTimeUpdate > 0) {
105
+ this.updating++
106
+ await timeout(this.platform.config.waitTimeUpdate)
107
+ if (--this.updating > 0) {
108
+ return
109
+ }
110
+ }
111
+ const targetConfig = this.targetConfig
112
+ this.targetConfig = {}
113
+ await this.put('/config', targetConfig)
114
+ // this.recentlyUpdated = true
115
+ // await timeout(500)
116
+ // this.recentlyUpdated = false
117
+ } catch (error) { this.warn(error) }
118
+ }
86
119
  }
87
120
 
88
121
  DeconzService.SensorsResource = SensorsResource
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "ebaauw"
8
8
  ],
9
9
  "license": "Apache-2.0",
10
- "version": "1.0.13",
10
+ "version": "1.0.17",
11
11
  "keywords": [
12
12
  "homebridge-plugin",
13
13
  "homekit",
@@ -26,13 +26,13 @@
26
26
  "ui": "cli/ui.js"
27
27
  },
28
28
  "engines": {
29
- "deCONZ": "2.27.6",
29
+ "deCONZ": "2.28.1",
30
30
  "homebridge": "^1.8.4||^2.0.0-beta",
31
31
  "node": "^20||^18"
32
32
  },
33
33
  "dependencies": {
34
34
  "hb-deconz-tools": "~2.0.3",
35
- "homebridge-lib": "~7.0.5"
35
+ "homebridge-lib": "~7.0.8"
36
36
  },
37
37
  "scripts": {
38
38
  "prepare": "standard && rm -rf out && jsdoc -c jsdoc.json",