homebridge-texecom-full 4.2.8 → 4.3.0-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/index.js +174 -275
- package/package.json +7 -6
package/index.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
var Service;
|
|
2
|
-
var Characteristic;
|
|
3
|
-
var Accessory;
|
|
4
1
|
var debug = require("debug")("TexecomAccessory");
|
|
5
2
|
var serialport = require("serialport");
|
|
6
3
|
var zpad = require("zpad");
|
|
@@ -14,67 +11,69 @@ const responseEmitter = new ResponseEmitter();
|
|
|
14
11
|
|
|
15
12
|
const LogUtil = require('./util/logutil');
|
|
16
13
|
|
|
17
|
-
var areas_armed = [];
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
var areas_armed = [];
|
|
20
15
|
var setByAlarm = false;
|
|
21
16
|
|
|
17
|
+
// ─── Homebridge v2 entry point ──────────────────────────────────────────────
|
|
18
|
+
module.exports = (api) => {
|
|
19
|
+
api.registerPlatform("Texecom", TexecomPlatform);
|
|
20
|
+
};
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
// ─── Platform ────────────────────────────────────────────────────────────────
|
|
23
|
+
class TexecomPlatform {
|
|
24
|
+
constructor(log, config, api) {
|
|
25
|
+
this.log = new LogUtil(config.debug, config.name, log);
|
|
26
|
+
this.api = api;
|
|
27
|
+
this.hap = api.hap;
|
|
28
|
+
|
|
29
|
+
this.serial_device = config["serial_device"];
|
|
30
|
+
this.baud_rate = config["baud_rate"];
|
|
31
|
+
this.zones = config["zones"] || [];
|
|
32
|
+
this.areas = config["areas"] || [];
|
|
33
|
+
this.ip_address = config["ip_address"];
|
|
34
|
+
this.ip_port = config["ip_port"];
|
|
35
|
+
this.udl = config["udl"];
|
|
36
|
+
|
|
37
|
+
this.api.on('didFinishLaunching', () => {
|
|
38
|
+
this._setupAccessories();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
config.debug,
|
|
35
|
-
config.name,
|
|
36
|
-
log
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
this.serial_device = config["serial_device"];
|
|
40
|
-
this.baud_rate = config["baud_rate"];
|
|
41
|
-
this.zones = config["zones"] || [];
|
|
42
|
-
this.areas = config["areas"] || [];
|
|
43
|
-
this.ip_address = config["ip_address"];
|
|
44
|
-
this.ip_port = config["ip_port"];
|
|
45
|
-
this.udl = config["udl"];
|
|
46
|
-
}
|
|
42
|
+
// Required by v2 — called for each cached accessory; we don't cache, so no-op
|
|
43
|
+
configureAccessory(accessory) { }
|
|
47
44
|
|
|
48
|
-
|
|
45
|
+
_setupAccessories() {
|
|
46
|
+
const platform = this;
|
|
47
|
+
const { hap, api } = this;
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
zoneAccessories.push(zone);
|
|
55
|
-
}
|
|
56
|
-
var zoneCount = zoneAccessories.length;
|
|
49
|
+
const zoneAccessories = this.zones.map(z => new TexecomAccessory(this.log, z, hap));
|
|
50
|
+
const areaAccessories = this.areas.map(a => new TexecomAccessory(this.log, a, hap));
|
|
51
|
+
const zoneCount = zoneAccessories.length;
|
|
52
|
+
const areaCount = areaAccessories.length;
|
|
57
53
|
|
|
54
|
+
// Publish all accessories as external accessories (no cache needed)
|
|
55
|
+
[...zoneAccessories, ...areaAccessories].forEach(acc => {
|
|
56
|
+
const uuid = hap.uuid.generate(acc.sn);
|
|
57
|
+
const hapAcc = new api.platformAccessory(acc.name, uuid);
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
for (var i = 0; i < this.areas.length; i++) {
|
|
61
|
-
var area = new TexecomAccessory(this.log, this.areas[i]);
|
|
62
|
-
areaAccessories.push(area);
|
|
63
|
-
}
|
|
64
|
-
var areaCount = areaAccessories.length;
|
|
59
|
+
const [infoSvc, mainSvc] = acc.getServices();
|
|
65
60
|
|
|
66
|
-
|
|
61
|
+
// Populate the built-in AccessoryInformation service
|
|
62
|
+
const infoTarget = hapAcc.getService(hap.Service.AccessoryInformation);
|
|
63
|
+
infoTarget
|
|
64
|
+
.setCharacteristic(hap.Characteristic.Manufacturer, "Homebridge")
|
|
65
|
+
.setCharacteristic(hap.Characteristic.Model, `Texecom ${acc.zone_type === "securitysystem" ? "Area" : "Zone"}`)
|
|
66
|
+
.setCharacteristic(hap.Characteristic.SerialNumber, acc.sn);
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
hapAcc.addService(mainSvc);
|
|
69
|
+
api.publishExternalAccessories("homebridge-texecom-full", [hapAcc]);
|
|
70
|
+
});
|
|
69
71
|
|
|
72
|
+
// ── Data processing ──────────────────────────────────────────────────
|
|
70
73
|
function processData(data) {
|
|
71
|
-
// Received data is a zone update
|
|
72
74
|
if (S(data).startsWith('"Z')) {
|
|
73
|
-
// Extract the data from the serial line received
|
|
74
75
|
var zone_data = Number(S(S(data).between('Z')).left(4).s);
|
|
75
|
-
// Extract the zone number that is being updated
|
|
76
76
|
var updated_zone = Number(S(S(data).between('Z')).left(3).s);
|
|
77
|
-
// Is the zone active?
|
|
78
77
|
var zone_active = S(zone_data).endsWith('1');
|
|
79
78
|
|
|
80
79
|
platform.log.debug(`Zone update received for zone ${updated_zone} active: ${zone_active}`);
|
|
@@ -89,106 +88,81 @@ TexecomPlatform.prototype = {
|
|
|
89
88
|
try {
|
|
90
89
|
areaAccessories[a].zones.forEach(zone => {
|
|
91
90
|
if (zone == zpad(updated_zone, 3) && is_armed(areaAccessories[a].zone_number)) {
|
|
92
|
-
|
|
93
|
-
// Set the security system state to "ALARM_TRIGGERED"
|
|
94
|
-
stateValue = Characteristic.SecuritySystemCurrentState.ALARM_TRIGGERED;
|
|
91
|
+
var stateValue = hap.Characteristic.SecuritySystemCurrentState.ALARM_TRIGGERED;
|
|
95
92
|
platform.log.log(`Area ${areaAccessories[a].zone_number} manual triggered`);
|
|
96
|
-
|
|
97
|
-
// Log the state change
|
|
98
|
-
platform.log.debug(`Area match found, updating area status in HomeKit to ${stateValue}`);
|
|
99
93
|
setByAlarm = true;
|
|
100
94
|
areaAccessories[a].changeHandler(stateValue);
|
|
101
|
-
|
|
102
95
|
}
|
|
103
96
|
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
console.debug(`Error processing zones for area ${a} Please add zones under area.`);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
console.debug(`Error processing zones for area ${a}: Please add zones under area.`);
|
|
107
99
|
}
|
|
108
100
|
}
|
|
109
101
|
}
|
|
110
102
|
break;
|
|
111
103
|
}
|
|
112
104
|
}
|
|
113
|
-
}
|
|
114
|
-
else if (S(data).startsWith('"A') || S(data).startsWith('"D') || S(data).startsWith('"L')) {
|
|
115
105
|
|
|
116
|
-
|
|
106
|
+
} else if (S(data).startsWith('"A') || S(data).startsWith('"D') || S(data).startsWith('"L')) {
|
|
107
|
+
const Characteristic = hap.Characteristic;
|
|
117
108
|
var updated_area = Number(S(S(data).substring(2, 5)));
|
|
118
109
|
var status = S(data).substring(1, 2);
|
|
119
110
|
var user = S(data).substring(5, 7);
|
|
120
111
|
var stateValue;
|
|
121
|
-
|
|
122
|
-
let armedByUser = user;
|
|
112
|
+
const armedByUser = user;
|
|
123
113
|
|
|
124
114
|
switch (String(status)) {
|
|
125
115
|
case "L":
|
|
126
116
|
stateValue = Characteristic.SecuritySystemCurrentState.ALARM_TRIGGERED;
|
|
127
117
|
platform.log.log(`Area ${updated_area} triggered`);
|
|
128
118
|
break;
|
|
119
|
+
|
|
129
120
|
case "D":
|
|
130
121
|
stateValue = Characteristic.SecuritySystemCurrentState.DISARMED;
|
|
131
122
|
platform.log.log(`Area ${updated_area} disarmed by User ${user}`);
|
|
132
|
-
areas_armed = areas_armed.filter(
|
|
123
|
+
areas_armed = areas_armed.filter(v => v !== zpad(updated_area, 3));
|
|
133
124
|
break;
|
|
134
|
-
case "A":
|
|
135
|
-
//user is for my setup
|
|
136
|
-
//my user 17 is full armed (remote)
|
|
137
|
-
//my user on app is 254
|
|
138
|
-
//all the rest is night armed
|
|
139
125
|
|
|
126
|
+
case "A":
|
|
140
127
|
if (user == "17") {
|
|
141
128
|
stateValue = Characteristic.SecuritySystemCurrentState.AWAY_ARM;
|
|
142
129
|
platform.log.log(`Area ${updated_area} armed (away) by User ${user}`);
|
|
143
|
-
}
|
|
144
|
-
else if (user == "25" || user == "254") {
|
|
145
|
-
// Read the target state
|
|
130
|
+
} else if (user == "25" || user == "254") {
|
|
146
131
|
const targetState = areaAccessories[updated_area - 1].target_State;
|
|
147
|
-
|
|
148
|
-
// Map the targetState to the corresponding current state value
|
|
149
132
|
switch (targetState) {
|
|
150
133
|
case Characteristic.SecuritySystemTargetState.AWAY_ARM:
|
|
151
|
-
stateValue = Characteristic.SecuritySystemCurrentState.AWAY_ARM;
|
|
152
|
-
break;
|
|
134
|
+
stateValue = Characteristic.SecuritySystemCurrentState.AWAY_ARM; break;
|
|
153
135
|
case Characteristic.SecuritySystemTargetState.STAY_ARM:
|
|
154
|
-
stateValue = Characteristic.SecuritySystemCurrentState.STAY_ARM;
|
|
155
|
-
break;
|
|
136
|
+
stateValue = Characteristic.SecuritySystemCurrentState.STAY_ARM; break;
|
|
156
137
|
case Characteristic.SecuritySystemTargetState.NIGHT_ARM:
|
|
157
|
-
stateValue = Characteristic.SecuritySystemCurrentState.NIGHT_ARM;
|
|
158
|
-
break;
|
|
138
|
+
stateValue = Characteristic.SecuritySystemCurrentState.NIGHT_ARM; break;
|
|
159
139
|
case Characteristic.SecuritySystemTargetState.DISARM:
|
|
160
|
-
stateValue = Characteristic.SecuritySystemCurrentState.DISARMED;
|
|
161
|
-
break;
|
|
140
|
+
stateValue = Characteristic.SecuritySystemCurrentState.DISARMED; break;
|
|
162
141
|
default:
|
|
163
142
|
platform.log.error(`Unknown target state: ${targetState}`);
|
|
164
143
|
return;
|
|
165
144
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
145
|
+
platform.log.log(`User ${user}: Target state ${targetState} → stateValue ${stateValue}`);
|
|
146
|
+
} else {
|
|
171
147
|
stateValue = Characteristic.SecuritySystemCurrentState.NIGHT_ARM;
|
|
172
148
|
platform.log.log(`Area ${updated_area} armed (night) by User ${user}`);
|
|
173
149
|
}
|
|
174
150
|
|
|
175
151
|
if (stateValue == Characteristic.SecuritySystemCurrentState.AWAY_ARM) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (index !== -1) {
|
|
180
|
-
// Replace the value at that index
|
|
181
|
-
areas_armed[index] = zpad(updated_area, 3);
|
|
152
|
+
const idx = areas_armed.findIndex(v => v === null || v === undefined);
|
|
153
|
+
if (idx !== -1) {
|
|
154
|
+
areas_armed[idx] = zpad(updated_area, 3);
|
|
182
155
|
} else {
|
|
183
|
-
// If no open slot, append to the end
|
|
184
156
|
areas_armed.push(zpad(updated_area, 3));
|
|
185
157
|
}
|
|
186
158
|
}
|
|
187
159
|
break;
|
|
160
|
+
|
|
188
161
|
default:
|
|
189
162
|
platform.log.log(`Unknown status letter ${status}`);
|
|
190
163
|
return;
|
|
191
164
|
}
|
|
165
|
+
|
|
192
166
|
for (var i = 0; i < areaCount; i++) {
|
|
193
167
|
if (areaAccessories[i].zone_number == updated_area) {
|
|
194
168
|
platform.log.debug(`Area match found, updating area status in HomeKit to ${stateValue}`);
|
|
@@ -197,21 +171,20 @@ TexecomPlatform.prototype = {
|
|
|
197
171
|
break;
|
|
198
172
|
}
|
|
199
173
|
}
|
|
174
|
+
|
|
200
175
|
} else {
|
|
201
176
|
platform.log.debug(`Unknown string from Texecom: ${S(data)}`);
|
|
202
177
|
}
|
|
203
178
|
}
|
|
204
179
|
|
|
180
|
+
// ── IP connection with auto-reconnect ────────────────────────────────
|
|
205
181
|
function setupConnection() {
|
|
206
182
|
platform.log.log('Attempting connection to Texecom...');
|
|
207
183
|
|
|
208
|
-
connection = net.createConnection(platform.ip_port, platform.ip_address);
|
|
209
|
-
|
|
184
|
+
var connection = net.createConnection(platform.ip_port, platform.ip_address);
|
|
210
185
|
connection.setNoDelay(true);
|
|
211
186
|
|
|
212
|
-
connection.on('connect', () =>
|
|
213
|
-
platform.log.log('Connected via IP');
|
|
214
|
-
});
|
|
187
|
+
connection.on('connect', () => platform.log.log('Connected via IP'));
|
|
215
188
|
|
|
216
189
|
connection.on('data', function (data) {
|
|
217
190
|
platform.log.debug(`IP data received: ${data}`);
|
|
@@ -221,117 +194,75 @@ TexecomPlatform.prototype = {
|
|
|
221
194
|
|
|
222
195
|
connection.on('error', (err) => {
|
|
223
196
|
platform.log.error('IP connection error:', err.message);
|
|
224
|
-
// We'll reconnect on 'close'
|
|
225
197
|
});
|
|
226
198
|
|
|
227
199
|
connection.on('close', () => {
|
|
228
|
-
platform.log.error('IP connection closed.
|
|
229
|
-
setTimeout(() =>
|
|
230
|
-
setupConnection(); // try again
|
|
231
|
-
}, 10000);
|
|
200
|
+
platform.log.error('IP connection closed. Reconnecting in 10s...');
|
|
201
|
+
setTimeout(() => setupConnection(), 10000);
|
|
232
202
|
});
|
|
233
203
|
|
|
234
|
-
connection.on('end', () =>
|
|
235
|
-
platform.log.log('IP connection ended');
|
|
236
|
-
});
|
|
204
|
+
connection.on('end', () => platform.log.log('IP connection ended'));
|
|
237
205
|
|
|
238
206
|
platform.texecomConnection = connection;
|
|
239
207
|
}
|
|
240
208
|
|
|
209
|
+
// ── Connect via serial or IP ─────────────────────────────────────────
|
|
241
210
|
if (this.serial_device) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
baudrate: this.baud_rate,
|
|
246
|
-
parser: serialport.parsers.readline("\n")
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
serialPort.on("open", function () {
|
|
250
|
-
platform.log.log("Serial port opened");
|
|
251
|
-
serialPort.on('data', function (data) {
|
|
252
|
-
platform.log.debug(`Serial data received: ${data}`);
|
|
253
|
-
responseEmitter.emit('data', data);
|
|
254
|
-
processData(data);
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
this.texecomConnection = serialPort;
|
|
258
|
-
} else if (this.ip_address) {
|
|
211
|
+
// serialport v12: baudRate (camelCase) + separate ReadlineParser
|
|
212
|
+
const { SerialPort } = require('serialport');
|
|
213
|
+
const { ReadlineParser } = require('@serialport/parser-readline');
|
|
259
214
|
|
|
260
|
-
|
|
215
|
+
const sp = new SerialPort({ path: this.serial_device, baudRate: this.baud_rate });
|
|
216
|
+
const parser = sp.pipe(new ReadlineParser({ delimiter: '\n' }));
|
|
261
217
|
|
|
218
|
+
sp.on("open", () => platform.log.log("Serial port opened"));
|
|
219
|
+
sp.on("error", (err) => platform.log.error("Serial port error:", err.message));
|
|
262
220
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
platform.log.log('Connected via IP');
|
|
266
|
-
});
|
|
267
|
-
} catch (err) {
|
|
268
|
-
platform.log.error(err);
|
|
269
|
-
}
|
|
270
|
-
connection.setNoDelay(true);
|
|
271
|
-
|
|
272
|
-
connection.on('data', function (data) {
|
|
273
|
-
platform.log.debug(`IP data received: ${data}`);
|
|
221
|
+
parser.on('data', function (data) {
|
|
222
|
+
platform.log.debug(`Serial data received: ${data}`);
|
|
274
223
|
responseEmitter.emit('data', data);
|
|
275
224
|
processData(data);
|
|
276
225
|
});
|
|
277
226
|
|
|
227
|
+
platform.texecomConnection = sp;
|
|
278
228
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
connection.on('end', function () {
|
|
285
|
-
platform.log.log('IP connection ended');
|
|
286
|
-
});
|
|
229
|
+
} else if (this.ip_address) {
|
|
230
|
+
setupConnection();
|
|
287
231
|
|
|
288
|
-
connection.on('close', function () {
|
|
289
|
-
platform.log.log('IP connection closed');
|
|
290
|
-
try {
|
|
291
|
-
connection = net.createConnection(platform.ip_port, platform.ip_address, function () {
|
|
292
|
-
platform.log.log('Re-connected after loss of connection');
|
|
293
|
-
});
|
|
294
|
-
} catch (err) {
|
|
295
|
-
platform.log.error(err);
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
|
-
this.texecomConnection = connection;*/
|
|
299
232
|
} else {
|
|
300
233
|
this.log.log("Must set either serial_device or ip_address in configuration.");
|
|
301
234
|
}
|
|
302
235
|
}
|
|
303
236
|
}
|
|
304
237
|
|
|
305
|
-
|
|
306
|
-
function TexecomAccessory(log, config) {
|
|
238
|
+
// ─── Accessory ───────────────────────────────────────────────────────────────
|
|
239
|
+
function TexecomAccessory(log, config, hap) {
|
|
307
240
|
this.log = log;
|
|
241
|
+
this.hap = hap;
|
|
308
242
|
|
|
309
243
|
this.zone_number = zpad(config["zone_number"] || config["area_number"], 3);
|
|
310
244
|
this.name = config["name"];
|
|
311
245
|
this.zone_type = config["zone_type"] || config["area_type"] || "motion";
|
|
312
246
|
this.dwell_time = config["dwell"] || 0;
|
|
247
|
+
this.dwell_timer = null;
|
|
313
248
|
|
|
314
249
|
if (config["area_type"] == "securitysystem") {
|
|
315
|
-
this.target_State = Characteristic.SecuritySystemTargetState.DISARM;
|
|
250
|
+
this.target_State = hap.Characteristic.SecuritySystemTargetState.DISARM;
|
|
316
251
|
}
|
|
252
|
+
|
|
317
253
|
try {
|
|
318
254
|
if (Array.isArray(config["zones"])) {
|
|
319
|
-
// If it's an array, apply zpad to each element
|
|
320
255
|
this.zones = config["zones"].map(zone => zpad(zone, 3));
|
|
321
256
|
} else {
|
|
322
|
-
// Otherwise, treat it as a single value
|
|
323
257
|
this.zones = zpad(config["zones"], 3);
|
|
324
258
|
}
|
|
325
|
-
} catch (e) {
|
|
326
|
-
//log.error('Error processing zones: ', e);
|
|
327
|
-
}
|
|
259
|
+
} catch (e) { /* no zones */ }
|
|
328
260
|
|
|
329
261
|
if (config["sn"]) {
|
|
330
262
|
this.sn = config["sn"];
|
|
331
263
|
} else {
|
|
332
|
-
|
|
333
|
-
shasum.update(this.zone_number
|
|
334
|
-
|
|
264
|
+
const shasum = crypto.createHash('sha1');
|
|
265
|
+
shasum.update(this.zone_number);
|
|
335
266
|
this.sn = shasum.digest('base64');
|
|
336
267
|
log.log(`Computed SN: ${this.sn}`);
|
|
337
268
|
}
|
|
@@ -340,212 +271,183 @@ function TexecomAccessory(log, config) {
|
|
|
340
271
|
TexecomAccessory.prototype = {
|
|
341
272
|
|
|
342
273
|
getServices: function () {
|
|
343
|
-
|
|
274
|
+
const { Service, Characteristic } = this.hap;
|
|
344
275
|
const me = this;
|
|
345
276
|
|
|
346
277
|
var service, changeAction;
|
|
347
278
|
|
|
279
|
+
// informationService is returned but the platform populates the
|
|
280
|
+
// built-in one on the platformAccessory; this keeps the shape identical
|
|
348
281
|
var informationService = new Service.AccessoryInformation();
|
|
349
|
-
|
|
350
282
|
informationService
|
|
351
|
-
.setCharacteristic(Characteristic.Name, this.name)
|
|
352
283
|
.setCharacteristic(Characteristic.Manufacturer, "Homebridge")
|
|
353
|
-
.setCharacteristic(Characteristic.Model, `Texecom ${
|
|
284
|
+
.setCharacteristic(Characteristic.Model, `Texecom ${this.zone_type === "securitysystem" ? "Area" : "Zone"}`)
|
|
354
285
|
.setCharacteristic(Characteristic.SerialNumber, this.sn);
|
|
355
286
|
|
|
356
|
-
|
|
357
287
|
switch (this.zone_type) {
|
|
358
288
|
case "contact":
|
|
359
289
|
service = new Service.ContactSensor();
|
|
360
290
|
changeAction = function (newState) {
|
|
361
291
|
service.getCharacteristic(Characteristic.ContactSensorState)
|
|
362
|
-
.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
case "motion":
|
|
366
|
-
service = new Service.MotionSensor();
|
|
367
|
-
changeAction = function (newState) {
|
|
368
|
-
service.getCharacteristic(Characteristic.MotionDetected)
|
|
369
|
-
.setValue(newState);
|
|
292
|
+
.updateValue(newState
|
|
293
|
+
? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
|
|
294
|
+
: Characteristic.ContactSensorState.CONTACT_DETECTED);
|
|
370
295
|
};
|
|
371
296
|
break;
|
|
297
|
+
|
|
372
298
|
case "smoke":
|
|
373
299
|
service = new Service.SmokeSensor();
|
|
374
300
|
changeAction = function (newState) {
|
|
375
301
|
service.getCharacteristic(Characteristic.SmokeDetected)
|
|
376
|
-
.
|
|
302
|
+
.updateValue(newState
|
|
303
|
+
? Characteristic.SmokeDetected.SMOKE_DETECTED
|
|
304
|
+
: Characteristic.SmokeDetected.SMOKE_NOT_DETECTED);
|
|
377
305
|
};
|
|
378
306
|
break;
|
|
307
|
+
|
|
379
308
|
case "carbonmonoxide":
|
|
380
309
|
service = new Service.CarbonMonoxideSensor();
|
|
381
310
|
changeAction = function (newState) {
|
|
382
311
|
service.getCharacteristic(Characteristic.CarbonMonoxideDetected)
|
|
383
|
-
.
|
|
312
|
+
.updateValue(newState
|
|
313
|
+
? Characteristic.CarbonMonoxideDetected.CO_LEVELS_ABNORMAL
|
|
314
|
+
: Characteristic.CarbonMonoxideDetected.CO_LEVELS_NORMAL);
|
|
384
315
|
};
|
|
385
316
|
break;
|
|
386
317
|
|
|
387
318
|
case "securitysystem":
|
|
388
319
|
service = new Service.SecuritySystem();
|
|
389
320
|
|
|
390
|
-
changeAction = function (newState
|
|
321
|
+
changeAction = function (newState) {
|
|
391
322
|
var targetState;
|
|
392
323
|
switch (newState) {
|
|
393
324
|
case Characteristic.SecuritySystemCurrentState.NIGHT_ARM:
|
|
394
|
-
targetState = Characteristic.SecuritySystemTargetState.NIGHT_ARM;
|
|
395
|
-
break;
|
|
325
|
+
targetState = Characteristic.SecuritySystemTargetState.NIGHT_ARM; break;
|
|
396
326
|
case Characteristic.SecuritySystemCurrentState.AWAY_ARM:
|
|
397
|
-
targetState = Characteristic.SecuritySystemTargetState.AWAY_ARM;
|
|
398
|
-
break;
|
|
327
|
+
targetState = Characteristic.SecuritySystemTargetState.AWAY_ARM; break;
|
|
399
328
|
case Characteristic.SecuritySystemCurrentState.STAY_ARM:
|
|
400
|
-
targetState = Characteristic.SecuritySystemTargetState.STAY_ARM;
|
|
401
|
-
break;
|
|
329
|
+
targetState = Characteristic.SecuritySystemTargetState.STAY_ARM; break;
|
|
402
330
|
case Characteristic.SecuritySystemCurrentState.DISARMED:
|
|
403
|
-
targetState = Characteristic.SecuritySystemTargetState.DISARM;
|
|
404
|
-
break;
|
|
331
|
+
targetState = Characteristic.SecuritySystemTargetState.DISARM; break;
|
|
405
332
|
default:
|
|
406
|
-
targetState = null;
|
|
407
|
-
break;
|
|
333
|
+
targetState = null; break;
|
|
408
334
|
}
|
|
409
|
-
service.getCharacteristic(Characteristic.SecuritySystemCurrentState).setValue(newState);
|
|
410
|
-
|
|
411
335
|
if (targetState != null) {
|
|
412
|
-
service.getCharacteristic(Characteristic.SecuritySystemTargetState).setValue(targetState);
|
|
413
336
|
service.getCharacteristic(Characteristic.SecuritySystemTargetState).updateValue(targetState);
|
|
414
337
|
}
|
|
415
338
|
service.getCharacteristic(Characteristic.SecuritySystemCurrentState).updateValue(newState);
|
|
416
|
-
me.log.debug(`Set target state ${targetState} and current state ${newState}
|
|
339
|
+
me.log.debug(`Set target state ${targetState} and current state ${newState}`);
|
|
417
340
|
};
|
|
418
341
|
|
|
419
|
-
//
|
|
342
|
+
// Safe default on startup
|
|
420
343
|
changeAction(Characteristic.SecuritySystemCurrentState.DISARMED);
|
|
344
|
+
|
|
421
345
|
var area = this;
|
|
422
346
|
|
|
347
|
+
// v2: onSet returns a Promise; deprecated on('set', cb) removed
|
|
423
348
|
service.getCharacteristic(Characteristic.SecuritySystemTargetState)
|
|
424
|
-
.
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
platform
|
|
434
|
-
|
|
435
|
-
|
|
349
|
+
.onSet(function (value) {
|
|
350
|
+
return new Promise((resolve, reject) => {
|
|
351
|
+
if (setByAlarm) {
|
|
352
|
+
me.log.debug(`Ignoring set — state change came from alarm itself.`);
|
|
353
|
+
setByAlarm = false;
|
|
354
|
+
resolve();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
// platform reference is injected after construction
|
|
358
|
+
const platform = me._platform;
|
|
359
|
+
if (platform && platform.udl != null) {
|
|
360
|
+
areaTargetSecurityStateSet(platform, area, service, value,
|
|
361
|
+
(err) => err ? reject(err) : resolve());
|
|
362
|
+
} else {
|
|
363
|
+
me.log.debug("No UDL configured. Add UDL to enable arm/disarm from HomeKit.");
|
|
364
|
+
reject(new Error("No UDL configured"));
|
|
365
|
+
}
|
|
366
|
+
});
|
|
436
367
|
});
|
|
437
368
|
break;
|
|
438
369
|
|
|
439
|
-
|
|
440
|
-
default:
|
|
370
|
+
default: // motion (and fallback)
|
|
441
371
|
service = new Service.MotionSensor();
|
|
442
372
|
changeAction = function (newState) {
|
|
443
|
-
service.getCharacteristic(Characteristic.MotionDetected)
|
|
444
|
-
.setValue(newState);
|
|
373
|
+
service.getCharacteristic(Characteristic.MotionDetected).updateValue(newState);
|
|
445
374
|
};
|
|
446
375
|
break;
|
|
447
376
|
}
|
|
448
377
|
|
|
449
|
-
this.changeHandler = function (status
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
this.dwell_timer = setTimeout(function () { changeAction(newState); }.bind(this), this.dwell_time);
|
|
378
|
+
this.changeHandler = function (status) {
|
|
379
|
+
const newState = status;
|
|
380
|
+
me.log.debug(`Dwell = ${me.dwell_time}`);
|
|
381
|
+
if (!newState && me.dwell_time > 0) {
|
|
382
|
+
me.dwell_timer = setTimeout(() => changeAction(newState), me.dwell_time);
|
|
455
383
|
} else {
|
|
456
|
-
if (
|
|
457
|
-
clearTimeout(this.dwell_timer);
|
|
458
|
-
}
|
|
384
|
+
if (me.dwell_timer) clearTimeout(me.dwell_timer);
|
|
459
385
|
changeAction(newState);
|
|
460
386
|
}
|
|
461
|
-
|
|
462
|
-
if (user == "unknown") {
|
|
463
|
-
platform.log.debug(`Changing state with changeHandler to ${newState}`);
|
|
464
|
-
}
|
|
465
|
-
else {
|
|
466
|
-
platform.log.debug(`Changing state with changeHandler to ${newState} by User ${user}`);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
}.bind(this);
|
|
387
|
+
};
|
|
470
388
|
|
|
471
389
|
return [informationService, service];
|
|
472
390
|
}
|
|
473
391
|
};
|
|
474
392
|
|
|
393
|
+
// ─── Arm/disarm command ───────────────────────────────────────────────────────
|
|
475
394
|
function areaTargetSecurityStateSet(platform, accessory, service, value, callback) {
|
|
395
|
+
const { Characteristic } = platform.hap;
|
|
476
396
|
|
|
477
397
|
const hexMapping = {
|
|
478
|
-
'1': 0x01,
|
|
479
|
-
'
|
|
480
|
-
'3': 0x04,
|
|
481
|
-
'4': 0x08,
|
|
482
|
-
'5': 0x10,
|
|
483
|
-
'6': 0x20,
|
|
484
|
-
'7': 0x40,
|
|
485
|
-
'8': 0x80
|
|
398
|
+
'1': 0x01, '2': 0x02, '3': 0x04, '4': 0x08,
|
|
399
|
+
'5': 0x10, '6': 0x20, '7': 0x40, '8': 0x80
|
|
486
400
|
};
|
|
487
401
|
|
|
488
402
|
var area_number = String.fromCharCode(parseInt(hexMapping[parseInt(accessory.zone_number, 10)], 16));
|
|
489
403
|
|
|
490
|
-
|
|
491
404
|
var command;
|
|
492
405
|
switch (value) {
|
|
493
406
|
case Characteristic.SecuritySystemTargetState.NIGHT_ARM:
|
|
494
407
|
case Characteristic.SecuritySystemTargetState.STAY_ARM:
|
|
495
|
-
command = `Y${area_number}`;
|
|
496
|
-
break;
|
|
408
|
+
command = `Y${area_number}`; break;
|
|
497
409
|
case Characteristic.SecuritySystemTargetState.AWAY_ARM:
|
|
498
|
-
command = `A${area_number}`;
|
|
499
|
-
break;
|
|
410
|
+
command = `A${area_number}`; break;
|
|
500
411
|
case Characteristic.SecuritySystemTargetState.DISARM:
|
|
501
|
-
command = `D${area_number}`;
|
|
502
|
-
break;
|
|
412
|
+
command = `D${area_number}`; break;
|
|
503
413
|
default:
|
|
504
|
-
|
|
414
|
+
platform.log.debug(`Unknown target state: ${value}`);
|
|
505
415
|
callback(new Error("Unknown target state"));
|
|
506
416
|
return;
|
|
507
417
|
}
|
|
508
418
|
|
|
509
|
-
platform.log.debug(`Sending
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
419
|
+
platform.log.debug(`Sending command ${value} to area ${accessory.zone_number}`);
|
|
513
420
|
|
|
514
421
|
writeCommandAndWaitForOK(platform.texecomConnection, `W${platform.udl}`)
|
|
515
422
|
.then(() => writeCommandAndWaitForOK(platform.texecomConnection, command, 0))
|
|
516
423
|
.then(() => {
|
|
517
|
-
// OK response from alarm is only indication that the target state has been reached
|
|
518
424
|
var currentState;
|
|
519
425
|
switch (value) {
|
|
520
426
|
case Characteristic.SecuritySystemTargetState.NIGHT_ARM:
|
|
521
|
-
currentState = Characteristic.SecuritySystemCurrentState.NIGHT_ARM;
|
|
522
|
-
break;
|
|
427
|
+
currentState = Characteristic.SecuritySystemCurrentState.NIGHT_ARM; break;
|
|
523
428
|
case Characteristic.SecuritySystemTargetState.AWAY_ARM:
|
|
524
|
-
currentState = Characteristic.SecuritySystemCurrentState.AWAY_ARM;
|
|
525
|
-
break;
|
|
429
|
+
currentState = Characteristic.SecuritySystemCurrentState.AWAY_ARM; break;
|
|
526
430
|
case Characteristic.SecuritySystemTargetState.STAY_ARM:
|
|
527
|
-
currentState = Characteristic.SecuritySystemCurrentState.STAY_ARM;
|
|
528
|
-
break;
|
|
431
|
+
currentState = Characteristic.SecuritySystemCurrentState.STAY_ARM; break;
|
|
529
432
|
case Characteristic.SecuritySystemTargetState.DISARM:
|
|
530
|
-
currentState = Characteristic.SecuritySystemCurrentState.DISARMED;
|
|
531
|
-
break;
|
|
433
|
+
currentState = Characteristic.SecuritySystemCurrentState.DISARMED; break;
|
|
532
434
|
default:
|
|
533
435
|
platform.log.debug(`Unknown target alarm state ${value}`);
|
|
534
436
|
callback(new Error("Unknown target state"));
|
|
535
437
|
return;
|
|
536
438
|
}
|
|
537
|
-
platform.log.debug(`
|
|
538
|
-
service.getCharacteristic(Characteristic.SecuritySystemCurrentState)
|
|
539
|
-
.updateValue(currentState);
|
|
439
|
+
platform.log.debug(`Area ${accessory.zone_number} → state ${currentState}`);
|
|
440
|
+
service.getCharacteristic(Characteristic.SecuritySystemCurrentState).updateValue(currentState);
|
|
540
441
|
accessory.target_State = currentState;
|
|
541
442
|
callback();
|
|
542
443
|
})
|
|
543
|
-
.catch(
|
|
544
|
-
platform.log.debug(`
|
|
545
|
-
callback(err);
|
|
444
|
+
.catch(err => {
|
|
445
|
+
platform.log.debug(`Command error: ${err}`);
|
|
446
|
+
callback(err);
|
|
546
447
|
});
|
|
547
448
|
}
|
|
548
449
|
|
|
450
|
+
// ─── Command writer ───────────────────────────────────────────────────────────
|
|
549
451
|
function writeCommandAndWaitForOK(connection, command, retryCount = 1) {
|
|
550
452
|
return new Promise((resolve, reject) => {
|
|
551
453
|
function handleData(data) {
|
|
@@ -559,18 +461,16 @@ function writeCommandAndWaitForOK(connection, command, retryCount = 1) {
|
|
|
559
461
|
|
|
560
462
|
connection.write(`\\${command}/`, function (err) {
|
|
561
463
|
if (err) {
|
|
562
|
-
|
|
464
|
+
responseEmitter.removeListener('data', handleData);
|
|
563
465
|
reject(err);
|
|
564
|
-
} else {
|
|
565
|
-
platform.log.debug(`Command sent: ${command}`);
|
|
566
466
|
}
|
|
567
467
|
});
|
|
568
468
|
|
|
569
469
|
setTimeout(() => {
|
|
570
470
|
responseEmitter.removeListener('data', handleData);
|
|
571
471
|
if (retryCount > 0) {
|
|
572
|
-
|
|
573
|
-
|
|
472
|
+
writeCommandAndWaitForOK(connection, command, retryCount - 1)
|
|
473
|
+
.then(resolve).catch(reject);
|
|
574
474
|
} else {
|
|
575
475
|
reject(new Error("Timeout after retries"));
|
|
576
476
|
}
|
|
@@ -578,8 +478,7 @@ function writeCommandAndWaitForOK(connection, command, retryCount = 1) {
|
|
|
578
478
|
});
|
|
579
479
|
}
|
|
580
480
|
|
|
481
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
581
482
|
function is_armed(area_number) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
return isAreaArmed;
|
|
483
|
+
return areas_armed.some(v => v === area_number);
|
|
585
484
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"displayName": "Homebridge Texecom",
|
|
3
3
|
"name": "homebridge-texecom-full",
|
|
4
4
|
"description": "A plugin for homebridge to integrate Texecom Premier Elite alarm systems into HomeKit",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.3.0-beta.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Posthumus",
|
|
8
8
|
"email": "chris.posthumus@gmail.com"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"url": "https://github.com/K1LL3R234/homebridge-texecom.git"
|
|
14
14
|
},
|
|
15
15
|
"bugs": {
|
|
16
|
-
"url": "https://
|
|
16
|
+
"url": "https://github.com/K1LL3R234/homebridge-texecom/issues"
|
|
17
17
|
},
|
|
18
18
|
"funding": [
|
|
19
19
|
{
|
|
@@ -39,9 +39,8 @@
|
|
|
39
39
|
"Texecom"
|
|
40
40
|
],
|
|
41
41
|
"engines": {
|
|
42
|
-
"homebridge": "
|
|
43
|
-
"node": ">=18.20.4"
|
|
44
|
-
"python": ">=3.6.0"
|
|
42
|
+
"homebridge": ">=2.0.0",
|
|
43
|
+
"node": ">=18.20.4"
|
|
45
44
|
},
|
|
46
45
|
"homebridge": {
|
|
47
46
|
"platforms": [
|
|
@@ -49,9 +48,11 @@
|
|
|
49
48
|
]
|
|
50
49
|
},
|
|
51
50
|
"dependencies": {
|
|
51
|
+
"@serialport/parser-readline": "^12.0.0",
|
|
52
|
+
"crypto-js": "^4.2.0",
|
|
52
53
|
"debug": "^4.2.0",
|
|
53
54
|
"serialport": "^12.0.0",
|
|
54
55
|
"string": ">=3.3.3",
|
|
55
56
|
"zpad": "^0.5.0"
|
|
56
57
|
}
|
|
57
|
-
}
|
|
58
|
+
}
|