homebridge-zencontrol-tpi 1.1.0-next.5 → 1.1.0-next.6

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
@@ -1,5 +1,11 @@
1
1
  # homebridge-zencontrol-tpi
2
2
 
3
+ ## 1.1.0-next.6
4
+
5
+ ### Minor Changes
6
+
7
+ - 1b01c14: Add windows
8
+
3
9
  ## 1.1.0-next.5
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -2,7 +2,80 @@
2
2
 
3
3
  A plugin for Homebridge that enables control over lights using Zencontrol Third Party Interface (TPI).
4
4
 
5
- ## Testing
5
+ ## Features
6
+
7
+ ### Groups
8
+
9
+ DALI Groups will be represented as light switches.
10
+
11
+ ### Individual lights
12
+
13
+ Any light ECGs that aren't part of a group will be represented as a light switch.
14
+
15
+ ### Blinds
16
+
17
+ Any blind controller that has its name (Location name) listed in the Blinds list in the plugin configuration will be represented
18
+ as a window covering.
19
+
20
+ If a system variable exists with the same name as the blind but with the word "Position" after it, then it will be used to reflect
21
+ the position of the blind (0 = closed, 100 = open). This is because the blind controller arc level may not accurately reflect the
22
+ blind position if it resets to 0 after some time.
23
+
24
+ If the position system variable is available, HomeKit will send a Recall Min to the blind (instead of the expected Off), and you must
25
+ setup a trigger + sequence to convert that to Off and to update the position system variable.
26
+
27
+ ### Windows
28
+
29
+ If you have windows controlled by a system variable pair—one for control and one for position—you can list the control variable name in
30
+ the plugin configuration.
31
+
32
+ The plugin assumes that the control system variable is set to:
33
+ * 0 for stopped
34
+ * 1 for closing
35
+ * 2 for opening
36
+
37
+ There should be a position system variable with the same name as the control variable with the word "Position" after it. This has
38
+ the same semantics as for blinds.
39
+
40
+ ### Relays
41
+
42
+ Any relay that has its name (Location name) listed in the Switches list in the plugin configuration will be represented as a switch. Only named relays
43
+ are handled to prevent pulling through relays that aren't appropriate.
44
+
45
+ ### Temperature
46
+
47
+ System variables that end with the word "Temperature" will be represented as temperature sensor accessories.
48
+
49
+ ### Humidity
50
+
51
+ System variables that end with the word "Humidity" will be represented as humidity sensor accessories.
52
+
53
+ ### Lux
54
+
55
+ System variables that end with the word "Lux" will be represented as light sensor accessories.
56
+
57
+ ### CO2
58
+
59
+ System variables that end with the word "CO2" will be represented as carbon dioxide sensor accessories.
60
+
61
+ Set the CO2 level to treat as abnormal in the plugin configuration.
62
+
63
+ ## Development
64
+
65
+ ```shell
66
+ nvm install
67
+ nvm use
68
+ npm install
69
+ npm run build
70
+ ```
71
+
72
+ When I make changes I like to test them on my local Homebridge, which is on another device accessible via ssh:
73
+
74
+ ```shell
75
+ npm run build && rsync -a dist ubuntu@192.168.1.2:/var/lib/homebridge/node_modules/homebridge-zencontrol-tpi/
76
+ ```
77
+
78
+ Then I restart Homebridge to load the updated code.
6
79
 
7
80
  ## Contributing
8
81
 
@@ -17,6 +17,14 @@
17
17
  "type": "string"
18
18
  }
19
19
  },
20
+ "windows": {
21
+ "type": "array",
22
+ "title": "Windows",
23
+ "description": "The names of system variables on your controllers that should be represented as windows.",
24
+ "items": {
25
+ "type": "string"
26
+ }
27
+ },
20
28
  "relays": {
21
29
  "type": "array",
22
30
  "title": "Switches",
@@ -1,8 +1,11 @@
1
+ const BLIND_OPEN = 100;
2
+ const BLIND_CLOSED = 0;
1
3
  export class ZencontrolBlindPlatformAccessory {
2
4
  constructor(platform, accessory) {
3
5
  this.platform = platform;
4
6
  this.accessory = accessory;
5
- this.currentPosition = 0; /* 0 = open, 100 = closed */
7
+ this.currentPosition = BLIND_OPEN;
8
+ this.positionState = this.platform.Characteristic.PositionState.STOPPED;
6
9
  this.accessory.getService(this.platform.Service.AccessoryInformation)
7
10
  .setCharacteristic(this.platform.Characteristic.Manufacturer, 'Zencontrol')
8
11
  .setCharacteristic(this.platform.Characteristic.Model, accessory.context.model || 'Unknown')
@@ -28,29 +31,89 @@ export class ZencontrolBlindPlatformAccessory {
28
31
  return this.targetPosition ?? 0;
29
32
  }
30
33
  async setTargetPosition(value) {
31
- const targetPosition = value;
32
- this.platform.log.debug(`Set blind ${this.accessory.displayName} (${this.accessory.context.address}) to ${targetPosition}`);
34
+ const targetPosition = value >= 50 ? BLIND_OPEN : BLIND_CLOSED;
35
+ this.platform.log.debug(`Set blind ${this.accessory.displayName} (${this.accessory.context.address}) to ${targetPosition === BLIND_OPEN ? 'open' : 'closed'}`);
33
36
  this.targetPosition = targetPosition;
37
+ if (this.positionStateTimeout) {
38
+ clearTimeout(this.positionStateTimeout);
39
+ this.positionStateTimeout = undefined;
40
+ }
34
41
  try {
35
- await this.platform.sendArcLevel(this.accessory.context.address, this.targetPosition ? 254 : 0, false);
42
+ if (this.targetPosition === BLIND_CLOSED) {
43
+ this.platform.log.debug(`Updating blind position state to decreasing: ${this.accessory.displayName}`);
44
+ this.positionState = this.platform.Characteristic.PositionState.DECREASING;
45
+ this.service.updateCharacteristic(this.platform.Characteristic.PositionState, this.positionState);
46
+ await this.platform.sendRecallMax(this.accessory.context.address);
47
+ }
48
+ else {
49
+ this.platform.log.debug(`Updating blind position state to increasing: ${this.accessory.displayName}`);
50
+ this.positionState = this.platform.Characteristic.PositionState.INCREASING;
51
+ this.service.updateCharacteristic(this.platform.Characteristic.PositionState, this.positionState);
52
+ if (this.positionSystemVariableAddress) {
53
+ await this.platform.sendRecallMin(this.accessory.context.address);
54
+ }
55
+ else {
56
+ await this.platform.sendOff(this.accessory.context.address);
57
+ }
58
+ }
59
+ this.positionStateTimeout = setTimeout(() => {
60
+ this.platform.log.debug(`Updating blind position state to stopped: ${this.accessory.displayName}`);
61
+ this.positionStateTimeout = undefined;
62
+ this.positionState = this.platform.Characteristic.PositionState.STOPPED;
63
+ this.service.updateCharacteristic(this.platform.Characteristic.PositionState, this.positionState);
64
+ }, 5000);
36
65
  }
37
66
  catch (error) {
38
- this.platform.log.warn(`Failed to update state for ${this.accessory.displayName}`, error);
67
+ this.platform.log.warn(`Failed to control blind ${this.accessory.displayName}`, error);
68
+ this.positionState = this.platform.Characteristic.PositionState.STOPPED;
69
+ this.service.updateCharacteristic(this.platform.Characteristic.PositionState, this.positionState);
39
70
  }
40
71
  }
41
72
  async getPositionState() {
42
- return this.platform.Characteristic.PositionState.STOPPED;
73
+ return this.positionState;
43
74
  }
75
+ /* NB: blind controllers change back to 0 after a while, so they inaccurately report that they're open; this is why we prefer the system variable. */
44
76
  async receiveArcLevel(arcLevel) {
45
- if (arcLevel === 255) {
46
- /* A stop fade; ignore */
77
+ if (this.positionSystemVariableAddress) {
78
+ this.platform.log.debug(`Controller updated blind ${this.accessory.displayName} to arc level ${arcLevel}; ignoring as there is a system variable configured`);
47
79
  return;
48
80
  }
49
- const value = arcLevel > 0 ? 100 : 0;
81
+ const value = arcLevel > 0 ? BLIND_CLOSED : BLIND_OPEN;
82
+ this.platform.log.debug(`Controller updated blind ${this.accessory.displayName} to ${value === BLIND_OPEN ? 'open' : 'closed'}`);
50
83
  if (value !== this.currentPosition) {
51
- this.platform.log.debug(`Controller updated blind ${this.accessory.displayName} to ${value}`);
52
84
  this.currentPosition = value;
53
85
  this.service.updateCharacteristic(this.platform.Characteristic.CurrentPosition, value);
54
86
  }
87
+ /* Update target position otherwise HomeKit will observe the difference between current and target and think the blind is moving */
88
+ if (value !== this.targetPosition) {
89
+ this.targetPosition = value;
90
+ this.service.updateCharacteristic(this.platform.Characteristic.TargetPosition, value);
91
+ }
92
+ }
93
+ async receivePosition(position) {
94
+ if (position < 0 || position > 100) {
95
+ this.platform.log.warn(`Ignoring invalid blind position for ${this.accessory.displayName}: ${position}`);
96
+ return;
97
+ }
98
+ this.platform.log.debug(`Controller updated blind ${this.accessory.displayName} to position ${position}`);
99
+ if (position !== this.currentPosition) {
100
+ this.currentPosition = position;
101
+ this.service.updateCharacteristic(this.platform.Characteristic.CurrentPosition, position);
102
+ }
103
+ /* Update target position otherwise HomeKit will observe the difference between current and target and think the blind is moving */
104
+ if (position !== this.targetPosition) {
105
+ this.targetPosition = position;
106
+ this.service.updateCharacteristic(this.platform.Characteristic.TargetPosition, position);
107
+ }
108
+ }
109
+ async receiveSystemVariableChange(systemVariableAddress, value) {
110
+ if (systemVariableAddress === this.positionSystemVariableAddress) {
111
+ if (value !== null) {
112
+ await this.receivePosition(value);
113
+ }
114
+ }
115
+ else {
116
+ this.platform.log.warn(`Ignoring unknown system variable change in blind "${this.displayName}: ${systemVariableAddress}`);
117
+ }
55
118
  }
56
119
  }
@@ -34,4 +34,7 @@ export class ZencontrolCO2PlatformAccessory {
34
34
  this.platform.log(`Received CO2 for ${this.displayName}: ${co2}`);
35
35
  this.service.updateCharacteristic(this.platform.Characteristic.CarbonDioxideLevel, co2);
36
36
  }
37
+ async receiveSystemVariableChange(systemVariableAddress, value) {
38
+ await this.receiveCO2(value);
39
+ }
37
40
  }
@@ -23,4 +23,7 @@ export class ZencontrolHumidityPlatformAccessory {
23
23
  this.platform.log(`Received humidity for ${this.displayName}: ${humidity}`);
24
24
  this.service.updateCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity, humidity);
25
25
  }
26
+ async receiveSystemVariableChange(systemVariableAddress, value) {
27
+ await this.receiveHumidity(value);
28
+ }
26
29
  }
@@ -27,4 +27,7 @@ export class ZencontrolLuxPlatformAccessory {
27
27
  this.platform.log(`Received lux for ${this.displayName}: ${lux}`);
28
28
  this.service.updateCharacteristic(this.platform.Characteristic.CurrentAmbientLightLevel, lux);
29
29
  }
30
+ async receiveSystemVariableChange(systemVariableAddress, value) {
31
+ this.receiveLux(value);
32
+ }
30
33
  }
package/dist/platform.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { ZencontrolLightPlatformAccessory } from './lightAccessory.js';
2
2
  import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
3
+ import { isZencontrolSystemVariableAccessory } from './types.js';
3
4
  import { ZenController, ZenProtocol, ZenAddress, ZenAddressType, ZenControlGearType, ZenConst } from 'zencontrol-tpi-node';
4
5
  import { ZencontrolTemperaturePlatformAccessory } from './temperatureAccessory.js';
5
6
  import { ZencontrolHumidityPlatformAccessory } from './humidityAccessory.js';
6
7
  import { ZencontrolRelayPlatformAccessory } from './relayAccessory.js';
7
8
  import { ZencontrolBlindPlatformAccessory } from './blindAccessory.js';
9
+ import { ZencontrolWindowPlatformAccessory } from './windowAccessory.js';
8
10
  import { ZencontrolLuxPlatformAccessory } from './luxAccessory.js';
9
11
  import { ZencontrolCO2PlatformAccessory } from './co2Accessory.js';
10
12
  /**
@@ -57,7 +59,9 @@ export class ZencontrolTPIPlatform {
57
59
  this.api.on('didFinishLaunching', () => {
58
60
  log.debug('Executed didFinishLaunching callback');
59
61
  // run the method to discover / register your devices as accessories
60
- this.discoverDevices();
62
+ this.discoverDevices().then(() => {
63
+ this.activateLiveEvents();
64
+ });
61
65
  });
62
66
  }
63
67
  /**
@@ -78,6 +82,7 @@ export class ZencontrolTPIPlatform {
78
82
  this.log.info('Discovering groups and devices');
79
83
  this.accessoriesByAddress.clear();
80
84
  const promises = [];
85
+ const positionVariables = [];
81
86
  for (const controller of this.zc.controllers) {
82
87
  /* Discover groups */
83
88
  promises.push(this.zc.queryGroupNumbers(controller).then((groups) => {
@@ -125,8 +130,8 @@ export class ZencontrolTPIPlatform {
125
130
  if (!label) {
126
131
  return;
127
132
  }
128
- if ((this.config.relays ?? []).indexOf(label) !== -1) {
129
- const acc = this.addRelayAccessory({
133
+ if ((this.config.blinds ?? []).indexOf(label) !== -1) {
134
+ const acc = this.addBlindAccessory({
130
135
  address: addressToString(ecg),
131
136
  label,
132
137
  model: 'Relay',
@@ -137,8 +142,8 @@ export class ZencontrolTPIPlatform {
137
142
  acc.receiveArcLevel(level);
138
143
  }
139
144
  }
140
- else if ((this.config.blinds ?? []).indexOf(label) !== -1) {
141
- const acc = this.addBlindAccessory({
145
+ else if ((this.config.relays ?? []).indexOf(label) !== -1) {
146
+ const acc = this.addRelayAccessory({
142
147
  address: addressToString(ecg),
143
148
  label,
144
149
  model: 'Relay',
@@ -161,51 +166,63 @@ export class ZencontrolTPIPlatform {
161
166
  }));
162
167
  for (let variable = 0; variable < ZenConst.MAX_SYSVAR; variable++) {
163
168
  promises.push(this.zc.querySystemVariableName(controller, variable).then(async (label) => {
164
- if (label && label.toLocaleLowerCase().endsWith(' temperature')) {
165
- let value = await this.zc.querySystemVariable(controller, variable);
166
- /* This API doesn't respect magnitude so we have to guess */
167
- if (value !== null) {
168
- while (value > 100) {
169
- value /= 10;
170
- }
171
- }
169
+ if (!label) {
170
+ return;
171
+ }
172
+ const address = systemVariableToAddressString(controller, variable);
173
+ if ((this.config.windows ?? []).indexOf(label) !== -1) {
174
+ const value = await this.zc.querySystemVariable(controller, variable);
175
+ const acc = this.addWindowAccessory({
176
+ address,
177
+ label,
178
+ model: 'System Variable',
179
+ serial: `SV ${controller.id}.${variable}`,
180
+ });
181
+ acc.receiveSystemVariableChange(address, value);
182
+ }
183
+ else if (label.toLocaleLowerCase().endsWith(' temperature')) {
184
+ const value = await this.zc.querySystemVariable(controller, variable);
172
185
  const acc = this.addTemperatureAccessory({
173
- address: systemVariableToAddressString(controller, variable),
186
+ address,
174
187
  label: label.substring(0, label.length - ' temperature'.length),
175
188
  model: 'System Variable',
176
189
  serial: `SV ${controller.id}.${variable}`,
177
190
  });
178
- acc.receiveTemperature(value);
191
+ acc.receiveSystemVariableChange(address, value);
179
192
  }
180
- else if (label && label.toLocaleLowerCase().endsWith(' humidity')) {
193
+ else if (label.toLocaleLowerCase().endsWith(' humidity')) {
181
194
  const value = await this.zc.querySystemVariable(controller, variable);
182
195
  const acc = this.addHumidityAccessory({
183
- address: systemVariableToAddressString(controller, variable),
196
+ address,
184
197
  label: label.substring(0, label.length - ' humidity'.length),
185
198
  model: 'System Variable',
186
199
  serial: `SV ${controller.id}.${variable}`,
187
200
  });
188
- acc.receiveHumidity(value);
201
+ acc.receiveSystemVariableChange(address, value);
189
202
  }
190
- else if (label && label.toLocaleLowerCase().endsWith(' lux')) {
203
+ else if (label.toLocaleLowerCase().endsWith(' lux')) {
191
204
  const value = await this.zc.querySystemVariable(controller, variable);
192
205
  const acc = this.addLuxAccessory({
193
- address: systemVariableToAddressString(controller, variable),
206
+ address,
194
207
  label: label.substring(0, label.length - ' lux'.length),
195
208
  model: 'System Variable',
196
209
  serial: `SV ${controller.id}.${variable}`,
197
210
  });
198
- acc.receiveLux(value);
211
+ acc.receiveSystemVariableChange(address, value);
199
212
  }
200
- else if (label && label.toLocaleLowerCase().endsWith(' co2')) {
213
+ else if (label.toLocaleLowerCase().endsWith(' co2')) {
201
214
  const value = await this.zc.querySystemVariable(controller, variable);
202
215
  const acc = this.addCO2Accessory({
203
- address: systemVariableToAddressString(controller, variable),
216
+ address,
204
217
  label: label.substring(0, label.length - ' co2'.length),
205
218
  model: 'System Variable',
206
219
  serial: `SV ${controller.id}.${variable}`,
207
220
  });
208
- acc.receiveCO2(value);
221
+ acc.receiveSystemVariableChange(address, value);
222
+ }
223
+ else if (label.toLocaleLowerCase().endsWith(' position')) {
224
+ const value = await this.zc.querySystemVariable(controller, variable);
225
+ positionVariables.push({ label, address, value });
209
226
  }
210
227
  }));
211
228
  }
@@ -218,6 +235,27 @@ export class ZencontrolTPIPlatform {
218
235
  /* Return so we don't remove accessories, as then the user will have to set them all up again! Adding them to rooms etc */
219
236
  return;
220
237
  }
238
+ /* Position variables; we come back and handle the position variables now that we've created all of the accessories */
239
+ for (const { label, address, value } of positionVariables) {
240
+ let foundAcc = undefined;
241
+ for (const [_, acc] of this.accessoriesByAddress) {
242
+ if (acc instanceof ZencontrolBlindPlatformAccessory || acc instanceof ZencontrolWindowPlatformAccessory) {
243
+ if ((acc.displayName + ' position').toLocaleLowerCase() === label.toLocaleLowerCase()) {
244
+ foundAcc = acc;
245
+ break;
246
+ }
247
+ }
248
+ }
249
+ if (foundAcc) {
250
+ this.log.info(`Found position system variable for ${foundAcc.displayName}: ${label}`);
251
+ foundAcc.positionSystemVariableAddress = address;
252
+ this.accessoriesByAddress.set(address, foundAcc);
253
+ foundAcc.receiveSystemVariableChange(address, value);
254
+ }
255
+ else {
256
+ this.log.debug(`Ignoring position system variable as no matching accessory found: ${label}`);
257
+ }
258
+ }
221
259
  // you can also deal with accessories from the cache which are no longer present by removing them from Homebridge
222
260
  // for example, if your plugin logs into a cloud account to retrieve a device list, and a user has previously removed a device
223
261
  // from this cloud account, then this device will no longer be present in the device list but will still be in the Homebridge cache
@@ -238,7 +276,6 @@ export class ZencontrolTPIPlatform {
238
276
  this.accessoryNeedsUpdate.splice(0, this.accessoryNeedsUpdate.length);
239
277
  }
240
278
  this.log.info('Device discovery complete');
241
- this.activateLiveEvents();
242
279
  }
243
280
  addLightAccessory({ address, label, model, serial, ...options }) {
244
281
  // generate a unique id for the accessory this should be generated from
@@ -314,6 +351,25 @@ export class ZencontrolTPIPlatform {
314
351
  this.discoveredCacheUUIDs.push(uuid);
315
352
  return acc;
316
353
  }
354
+ addWindowAccessory({ address, label, model, serial }) {
355
+ const uuid = this.api.hap.uuid.generate(`window @ ${address}`);
356
+ const existingAccessory = this.accessories.get(uuid);
357
+ let acc;
358
+ if (existingAccessory) {
359
+ this.log.debug('Restoring existing window accessory from cache:', existingAccessory.displayName);
360
+ this.updateAccessory(existingAccessory, { address, label, model, serial });
361
+ acc = new ZencontrolWindowPlatformAccessory(this, existingAccessory, address);
362
+ }
363
+ else {
364
+ this.log.info('Adding new window accessory:', label);
365
+ const accessory = new this.api.platformAccessory(label, uuid);
366
+ this.setupAccessory(accessory, { address, label, model, serial });
367
+ acc = new ZencontrolWindowPlatformAccessory(this, accessory, address);
368
+ }
369
+ this.accessoriesByAddress.set(address, acc);
370
+ this.discoveredCacheUUIDs.push(uuid);
371
+ return acc;
372
+ }
317
373
  addTemperatureAccessory({ address, label, model, serial }) {
318
374
  const uuid = this.api.hap.uuid.generate(`temperature @ ${address}`);
319
375
  const existingAccessory = this.accessories.get(uuid);
@@ -458,27 +514,18 @@ export class ZencontrolTPIPlatform {
458
514
  }
459
515
  };
460
516
  this.zc.systemVariableChangeCallback = (controller, variable, value) => {
461
- const accessoryId = systemVariableToAddressString(controller, variable);
462
- const acc = this.accessoriesByAddress.get(accessoryId);
463
- if (acc instanceof ZencontrolTemperaturePlatformAccessory) {
464
- acc.receiveTemperature(value).catch((reason) => {
465
- this.log.warn(`Failed to update temperature accessory "${acc.displayName}" color: ${reason}`);
466
- });
467
- }
468
- else if (acc instanceof ZencontrolHumidityPlatformAccessory) {
469
- acc.receiveHumidity(value).catch((reason) => {
470
- this.log.warn(`Failed to update humidity accessory "${acc.displayName}" color: ${reason}`);
471
- });
517
+ const variableAddress = systemVariableToAddressString(controller, variable);
518
+ const acc = this.accessoriesByAddress.get(variableAddress);
519
+ if (!acc) {
520
+ return;
472
521
  }
473
- else if (acc instanceof ZencontrolLuxPlatformAccessory) {
474
- acc.receiveLux(value).catch((reason) => {
475
- this.log.warn(`Failed to update lux accessory "${acc.displayName}" color: ${reason}`);
522
+ if (isZencontrolSystemVariableAccessory(acc)) {
523
+ acc.receiveSystemVariableChange(variableAddress, value).catch((reason) => {
524
+ this.log.warn(`Failed to update accessory "${acc.displayName}": ${reason}`);
476
525
  });
477
526
  }
478
- else if (acc instanceof ZencontrolCO2PlatformAccessory) {
479
- acc.receiveCO2(value).catch((reason) => {
480
- this.log.warn(`Failed to update CO2 accessory "${acc.displayName}" color: ${reason}`);
481
- });
527
+ else {
528
+ this.log.warn(`Received system variable change for unsupported accessory: ${acc?.displayName}`);
482
529
  }
483
530
  };
484
531
  this.log.info('Starting live event monitoring');
@@ -490,12 +537,63 @@ export class ZencontrolTPIPlatform {
490
537
  await this.applyInstant(accessoryId, address);
491
538
  }
492
539
  try {
493
- await this.zc.daliArcLevel(address, arcLevel);
540
+ const result = await this.zc.daliArcLevel(address, arcLevel);
541
+ if (!result) {
542
+ this.log.warn(`Failed to send arc level ${arcLevel} for ${address}`);
543
+ }
494
544
  }
495
545
  catch (error) {
496
546
  this.log.warn(`Failed to send arc level for ${address}:`, error);
497
547
  }
498
548
  }
549
+ async sendOff(accessoryId) {
550
+ const address = this.parseAccessoryId(accessoryId);
551
+ try {
552
+ const result = await this.zc.daliOff(address);
553
+ if (!result) {
554
+ this.log.warn(`Failed to send off for ${address}`);
555
+ }
556
+ }
557
+ catch (error) {
558
+ this.log.warn(`Failed to send off for ${address}:`, error);
559
+ }
560
+ }
561
+ async sendRecallMin(accessoryId) {
562
+ const address = this.parseAccessoryId(accessoryId);
563
+ try {
564
+ const result = await this.zc.daliRecallMin(address);
565
+ if (!result) {
566
+ this.log.warn(`Failed to send recall min for ${address}`);
567
+ }
568
+ }
569
+ catch (error) {
570
+ this.log.warn(`Failed to send recall min for ${address}:`, error);
571
+ }
572
+ }
573
+ async sendRecallMax(accessoryId) {
574
+ const address = this.parseAccessoryId(accessoryId);
575
+ try {
576
+ const result = await this.zc.daliRecallMax(address);
577
+ if (!result) {
578
+ this.log.warn(`Failed to send recall max for ${address}`);
579
+ }
580
+ }
581
+ catch (error) {
582
+ this.log.warn(`Failed to send recall max for ${address}:`, error);
583
+ }
584
+ }
585
+ async setSystemVariable(address, value) {
586
+ const { controller, variable } = this.parseSystemVariableAddress(address);
587
+ try {
588
+ const result = await this.zc.setSystemVariable(controller, variable, value);
589
+ if (!result) {
590
+ this.log.warn(`Failed to set system variable ${controller.id}.${variable} to ${value}`);
591
+ }
592
+ }
593
+ catch (error) {
594
+ this.log.warn(`Failed to set system variable ${controller.id}.${variable} to ${value}: ${error}`);
595
+ }
596
+ }
499
597
  async sendColor(accessoryId, color, arcLevel, instant = true) {
500
598
  const address = this.parseAccessoryId(accessoryId);
501
599
  try {
@@ -545,6 +643,23 @@ export class ZencontrolTPIPlatform {
545
643
  throw new Error(`Unrecognised accessory ID: ${accessoryId}`);
546
644
  }
547
645
  }
646
+ parseSystemVariableAddress(address) {
647
+ const parts = address.split(' ');
648
+ if (parts.length < 2) {
649
+ throw new Error(`Unrecognised system variable adddress: ${address}`);
650
+ }
651
+ const controllerId = parseInt(parts[1]);
652
+ const controller = this.zc.controllers.find(c => c.id === controllerId);
653
+ if (!controller) {
654
+ throw new Error(`Unknown controller id: ${controllerId}`);
655
+ }
656
+ if (parts[0] === 'SV') {
657
+ return { controller, variable: Number(parts[2]) };
658
+ }
659
+ else {
660
+ throw new Error(`Unrecognised system variable address: ${address}`);
661
+ }
662
+ }
548
663
  }
549
664
  function addressToString(address) {
550
665
  switch (address.type) {
@@ -29,4 +29,7 @@ export class ZencontrolTemperaturePlatformAccessory {
29
29
  this.platform.log(`Received temperature for ${this.displayName}: ${temperature}`);
30
30
  this.service.updateCharacteristic(this.platform.Characteristic.CurrentTemperature, temperature);
31
31
  }
32
+ async receiveSystemVariableChange(systemVariableAddress, value) {
33
+ this.receiveTemperature(value);
34
+ }
32
35
  }
package/dist/types.js CHANGED
@@ -1 +1,4 @@
1
- export {};
1
+ export function isZencontrolSystemVariableAccessory(acc) {
2
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3
+ return (typeof acc.receiveSystemVariableChange === 'function');
4
+ }
@@ -0,0 +1,119 @@
1
+ const WINDOW_OPEN = 100;
2
+ const WINDOW_CLOSED = 0;
3
+ const WINDOW_OPENING = 2;
4
+ const WINDOW_CLOSING = 1;
5
+ /**
6
+ * Handle windows represented by a control system variable and a position system variable.
7
+ */
8
+ export class ZencontrolWindowPlatformAccessory {
9
+ constructor(platform, accessory, controlSystemVariableAddress) {
10
+ this.platform = platform;
11
+ this.accessory = accessory;
12
+ this.controlSystemVariableAddress = controlSystemVariableAddress;
13
+ this.currentPosition = WINDOW_OPEN;
14
+ this.positionState = this.platform.Characteristic.PositionState.STOPPED;
15
+ this.accessory.getService(this.platform.Service.AccessoryInformation)
16
+ .setCharacteristic(this.platform.Characteristic.Manufacturer, 'Zencontrol')
17
+ .setCharacteristic(this.platform.Characteristic.Model, accessory.context.model || 'Unknown')
18
+ .setCharacteristic(this.platform.Characteristic.SerialNumber, accessory.context.serial || 'Unknown');
19
+ this.service = this.accessory.getService(this.platform.Service.Window) || this.accessory.addService(this.platform.Service.Window);
20
+ this.service.setCharacteristic(this.platform.Characteristic.Name, accessory.displayName);
21
+ // https://developers.homebridge.io/#/service/Window
22
+ this.service.getCharacteristic(this.platform.Characteristic.CurrentPosition)
23
+ .onGet(this.getCurrentPosition.bind(this));
24
+ this.service.getCharacteristic(this.platform.Characteristic.TargetPosition)
25
+ .onGet(this.getTargetPosition.bind(this))
26
+ .onSet(this.setTargetPosition.bind(this));
27
+ this.service.getCharacteristic(this.platform.Characteristic.PositionState)
28
+ .onGet(this.getPositionState.bind(this));
29
+ }
30
+ get displayName() {
31
+ return this.accessory.displayName;
32
+ }
33
+ async getCurrentPosition() {
34
+ return this.currentPosition;
35
+ }
36
+ async getTargetPosition() {
37
+ return this.targetPosition ?? 0;
38
+ }
39
+ async setTargetPosition(value) {
40
+ const targetPosition = value >= 50 ? WINDOW_OPEN : WINDOW_CLOSED;
41
+ this.platform.log.debug(`Set window ${this.accessory.displayName} (${this.accessory.context.address}) to ${targetPosition === WINDOW_OPEN ? 'open' : 'closed'}`);
42
+ this.targetPosition = targetPosition;
43
+ if (this.positionStateTimeout) {
44
+ clearTimeout(this.positionStateTimeout);
45
+ this.positionStateTimeout = undefined;
46
+ }
47
+ try {
48
+ if (this.targetPosition === WINDOW_CLOSED) {
49
+ this.platform.log.debug(`Updating window position state to decreasing: ${this.accessory.displayName}`);
50
+ this.positionState = this.platform.Characteristic.PositionState.DECREASING;
51
+ this.service.updateCharacteristic(this.platform.Characteristic.PositionState, this.positionState);
52
+ await this.platform.setSystemVariable(this.controlSystemVariableAddress, WINDOW_CLOSING);
53
+ }
54
+ else {
55
+ this.platform.log.debug(`Updating window position state to increasing: ${this.accessory.displayName}`);
56
+ this.positionState = this.platform.Characteristic.PositionState.INCREASING;
57
+ this.service.updateCharacteristic(this.platform.Characteristic.PositionState, this.positionState);
58
+ await this.platform.setSystemVariable(this.controlSystemVariableAddress, WINDOW_OPENING);
59
+ }
60
+ this.positionStateTimeout = setTimeout(() => {
61
+ this.platform.log.debug(`Updating window position state to stopped: ${this.accessory.displayName}`);
62
+ this.positionStateTimeout = undefined;
63
+ this.positionState = this.platform.Characteristic.PositionState.STOPPED;
64
+ this.service.updateCharacteristic(this.platform.Characteristic.PositionState, this.positionState);
65
+ }, 5000);
66
+ }
67
+ catch (error) {
68
+ this.platform.log.warn(`Failed to control window ${this.accessory.displayName}`, error);
69
+ this.positionState = this.platform.Characteristic.PositionState.STOPPED;
70
+ this.service.updateCharacteristic(this.platform.Characteristic.PositionState, this.positionState);
71
+ }
72
+ }
73
+ async getPositionState() {
74
+ return this.positionState;
75
+ }
76
+ async receiveControl(control) {
77
+ if (control < 0 || control > 2) {
78
+ this.platform.log.warn(`Ignoring invalid window control for ${this.accessory.displayName}: ${control}`);
79
+ return;
80
+ }
81
+ this.platform.log.debug(`Controller updated window ${this.accessory.displayName} to ${control === 0 ? 'stopped' : control === 1 ? 'closing' : 'opening'}`);
82
+ const position = control === 0 ? -1 : control === 1 ? WINDOW_CLOSED : WINDOW_OPEN;
83
+ if (position !== -1 && position !== this.targetPosition) {
84
+ this.targetPosition = position;
85
+ this.service.updateCharacteristic(this.platform.Characteristic.TargetPosition, position);
86
+ }
87
+ }
88
+ async receivePosition(position) {
89
+ if (position < 0 || position > 100) {
90
+ this.platform.log.warn(`Ignoring invalid window position for ${this.accessory.displayName}: ${position}`);
91
+ return;
92
+ }
93
+ this.platform.log.debug(`Controller updated window ${this.accessory.displayName} to position ${position}`);
94
+ if (position !== this.currentPosition) {
95
+ this.currentPosition = position;
96
+ this.service.updateCharacteristic(this.platform.Characteristic.CurrentPosition, position);
97
+ }
98
+ /* Update target position otherwise HomeKit will observe the difference between current and target and think the blind is moving */
99
+ if (position !== this.targetPosition) {
100
+ this.targetPosition = position;
101
+ this.service.updateCharacteristic(this.platform.Characteristic.TargetPosition, position);
102
+ }
103
+ }
104
+ async receiveSystemVariableChange(systemVariableAddress, value) {
105
+ if (systemVariableAddress === this.controlSystemVariableAddress) {
106
+ if (value !== null) {
107
+ this.receiveControl(value);
108
+ }
109
+ }
110
+ else if (systemVariableAddress === this.positionSystemVariableAddress) {
111
+ if (value !== null) {
112
+ this.receivePosition(value);
113
+ }
114
+ }
115
+ else {
116
+ this.platform.log.warn(`Ignoring unknown system variable change in blind "${this.displayName}: ${systemVariableAddress}`);
117
+ }
118
+ }
119
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-zencontrol-tpi",
3
- "version": "1.1.0-next.5",
3
+ "version": "1.1.0-next.6",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "homebridge-lib": "^7.2.0",
37
- "zencontrol-tpi-node": "^1.1.0"
37
+ "zencontrol-tpi-node": "^1.2.0"
38
38
  },
39
39
  "publishConfig": {
40
40
  "access": "public"