homebridge-zencontrol-tpi 1.1.0-next.4 → 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 +12 -0
- package/README.md +74 -1
- package/config.schema.json +8 -0
- package/dist/blindAccessory.js +73 -10
- package/dist/co2Accessory.js +3 -0
- package/dist/humidityAccessory.js +3 -0
- package/dist/luxAccessory.js +3 -0
- package/dist/platform.js +160 -45
- package/dist/temperatureAccessory.js +3 -0
- package/dist/types.js +4 -1
- package/dist/windowAccessory.js +119 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# homebridge-zencontrol-tpi
|
|
2
2
|
|
|
3
|
+
## 1.1.0-next.6
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1b01c14: Add windows
|
|
8
|
+
|
|
9
|
+
## 1.1.0-next.5
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 9e59549: Fix blind accessory to have different UUID so it doesn't collide with relay accessory if it already exists
|
|
14
|
+
|
|
3
15
|
## 1.1.0-next.4
|
|
4
16
|
|
|
5
17
|
### Minor 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
|
-
##
|
|
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
|
|
package/config.schema.json
CHANGED
|
@@ -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",
|
package/dist/blindAccessory.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
|
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.
|
|
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 (
|
|
46
|
-
|
|
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 ?
|
|
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
|
}
|
package/dist/co2Accessory.js
CHANGED
|
@@ -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
|
}
|
package/dist/luxAccessory.js
CHANGED
|
@@ -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.
|
|
129
|
-
const acc = this.
|
|
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.
|
|
141
|
-
const acc = this.
|
|
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
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
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.
|
|
191
|
+
acc.receiveSystemVariableChange(address, value);
|
|
179
192
|
}
|
|
180
|
-
else if (label
|
|
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
|
|
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.
|
|
201
|
+
acc.receiveSystemVariableChange(address, value);
|
|
189
202
|
}
|
|
190
|
-
else if (label
|
|
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
|
|
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.
|
|
211
|
+
acc.receiveSystemVariableChange(address, value);
|
|
199
212
|
}
|
|
200
|
-
else if (label
|
|
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
|
|
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.
|
|
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
|
|
@@ -296,7 +333,7 @@ export class ZencontrolTPIPlatform {
|
|
|
296
333
|
return acc;
|
|
297
334
|
}
|
|
298
335
|
addBlindAccessory({ address, label, model, serial }) {
|
|
299
|
-
const uuid = this.api.hap.uuid.generate(`
|
|
336
|
+
const uuid = this.api.hap.uuid.generate(`blind @ ${address}`);
|
|
300
337
|
const existingAccessory = this.accessories.get(uuid);
|
|
301
338
|
let acc;
|
|
302
339
|
if (existingAccessory) {
|
|
@@ -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
|
|
462
|
-
const acc = this.accessoriesByAddress.get(
|
|
463
|
-
if (acc
|
|
464
|
-
|
|
465
|
-
this.log.warn(`Failed to update temperature accessory "${acc.displayName}" color: ${reason}`);
|
|
466
|
-
});
|
|
517
|
+
const variableAddress = systemVariableToAddressString(controller, variable);
|
|
518
|
+
const acc = this.accessoriesByAddress.get(variableAddress);
|
|
519
|
+
if (!acc) {
|
|
520
|
+
return;
|
|
467
521
|
}
|
|
468
|
-
|
|
469
|
-
acc.
|
|
470
|
-
this.log.warn(`Failed to update
|
|
522
|
+
if (isZencontrolSystemVariableAccessory(acc)) {
|
|
523
|
+
acc.receiveSystemVariableChange(variableAddress, value).catch((reason) => {
|
|
524
|
+
this.log.warn(`Failed to update accessory "${acc.displayName}": ${reason}`);
|
|
471
525
|
});
|
|
472
526
|
}
|
|
473
|
-
else
|
|
474
|
-
|
|
475
|
-
this.log.warn(`Failed to update lux accessory "${acc.displayName}" color: ${reason}`);
|
|
476
|
-
});
|
|
477
|
-
}
|
|
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
|
@@ -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.
|
|
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.
|
|
37
|
+
"zencontrol-tpi-node": "^1.2.0"
|
|
38
38
|
},
|
|
39
39
|
"publishConfig": {
|
|
40
40
|
"access": "public"
|