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