homebridge-texecom-full 4.2.7 → 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/CHANGELOG.md +5 -0
- package/index.js +190 -290
- package/package.json +7 -6
package/CHANGELOG.md
CHANGED
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
for (var i = 0; i < this.zones.length; i++) {
|
|
53
|
-
var zone = new TexecomAccessory(this.log, this.zones[i]);
|
|
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,140 +171,98 @@ 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
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
serialPort = new SerialPort(this.serial_device, {
|
|
209
|
-
baudrate: this.baud_rate,
|
|
210
|
-
parser: serialport.parsers.readline("\n")
|
|
211
|
-
});
|
|
180
|
+
// ── IP connection with auto-reconnect ────────────────────────────────
|
|
181
|
+
function setupConnection() {
|
|
182
|
+
platform.log.log('Attempting connection to Texecom...');
|
|
212
183
|
|
|
213
|
-
|
|
214
|
-
platform.log.log("Serial port opened");
|
|
215
|
-
serialPort.on('data', function (data) {
|
|
216
|
-
platform.log.debug(`Serial data received: ${data}`);
|
|
217
|
-
responseEmitter.emit('data', data);
|
|
218
|
-
processData(data);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
this.texecomConnection = serialPort;
|
|
222
|
-
} else if (this.ip_address) {
|
|
223
|
-
|
|
224
|
-
setupConnection();
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
/*try {
|
|
228
|
-
connection = net.createConnection(platform.ip_port, platform.ip_address, function () {
|
|
229
|
-
platform.log.log('Connected via IP');
|
|
230
|
-
});
|
|
231
|
-
} catch (err) {
|
|
232
|
-
platform.log.error(err);
|
|
233
|
-
}
|
|
184
|
+
var connection = net.createConnection(platform.ip_port, platform.ip_address);
|
|
234
185
|
connection.setNoDelay(true);
|
|
235
186
|
|
|
187
|
+
connection.on('connect', () => platform.log.log('Connected via IP'));
|
|
188
|
+
|
|
236
189
|
connection.on('data', function (data) {
|
|
237
190
|
platform.log.debug(`IP data received: ${data}`);
|
|
238
191
|
responseEmitter.emit('data', data);
|
|
239
192
|
processData(data);
|
|
240
193
|
});
|
|
241
194
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
platform.log.error(`IP connection error: ${err}`);
|
|
245
|
-
|
|
195
|
+
connection.on('error', (err) => {
|
|
196
|
+
platform.log.error('IP connection error:', err.message);
|
|
246
197
|
});
|
|
247
198
|
|
|
248
|
-
connection.on('
|
|
249
|
-
platform.log.
|
|
199
|
+
connection.on('close', () => {
|
|
200
|
+
platform.log.error('IP connection closed. Reconnecting in 10s...');
|
|
201
|
+
setTimeout(() => setupConnection(), 10000);
|
|
250
202
|
});
|
|
251
203
|
|
|
252
|
-
connection.on('
|
|
253
|
-
platform.log.log('IP connection closed');
|
|
254
|
-
try {
|
|
255
|
-
connection = net.createConnection(platform.ip_port, platform.ip_address, function () {
|
|
256
|
-
platform.log.log('Re-connected after loss of connection');
|
|
257
|
-
});
|
|
258
|
-
} catch (err) {
|
|
259
|
-
platform.log.error(err);
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
this.texecomConnection = connection;*/
|
|
263
|
-
} else {
|
|
264
|
-
this.log.log("Must set either serial_device or ip_address in configuration.");
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function setupConnection() {
|
|
270
|
-
platform.log.log('Attempting connection to Texecom...');
|
|
204
|
+
connection.on('end', () => platform.log.log('IP connection ended'));
|
|
271
205
|
|
|
272
|
-
|
|
206
|
+
platform.texecomConnection = connection;
|
|
207
|
+
}
|
|
273
208
|
|
|
274
|
-
|
|
209
|
+
// ── Connect via serial or IP ─────────────────────────────────────────
|
|
210
|
+
if (this.serial_device) {
|
|
211
|
+
// serialport v12: baudRate (camelCase) + separate ReadlineParser
|
|
212
|
+
const { SerialPort } = require('serialport');
|
|
213
|
+
const { ReadlineParser } = require('@serialport/parser-readline');
|
|
275
214
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
});
|
|
215
|
+
const sp = new SerialPort({ path: this.serial_device, baudRate: this.baud_rate });
|
|
216
|
+
const parser = sp.pipe(new ReadlineParser({ delimiter: '\n' }));
|
|
279
217
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
responseEmitter.emit('data', data);
|
|
283
|
-
processData(data);
|
|
284
|
-
});
|
|
218
|
+
sp.on("open", () => platform.log.log("Serial port opened"));
|
|
219
|
+
sp.on("error", (err) => platform.log.error("Serial port error:", err.message));
|
|
285
220
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
221
|
+
parser.on('data', function (data) {
|
|
222
|
+
platform.log.debug(`Serial data received: ${data}`);
|
|
223
|
+
responseEmitter.emit('data', data);
|
|
224
|
+
processData(data);
|
|
225
|
+
});
|
|
290
226
|
|
|
291
|
-
|
|
292
|
-
platform.log.error('IP connection closed. Will attempt to reconnect in 10s.');
|
|
293
|
-
setTimeout(() => {
|
|
294
|
-
setupConnection(); // try again
|
|
295
|
-
}, 10000);
|
|
296
|
-
});
|
|
227
|
+
platform.texecomConnection = sp;
|
|
297
228
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
});
|
|
229
|
+
} else if (this.ip_address) {
|
|
230
|
+
setupConnection();
|
|
301
231
|
|
|
302
|
-
|
|
232
|
+
} else {
|
|
233
|
+
this.log.log("Must set either serial_device or ip_address in configuration.");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
303
236
|
}
|
|
304
237
|
|
|
305
|
-
|
|
238
|
+
// ─── Accessory ───────────────────────────────────────────────────────────────
|
|
239
|
+
function TexecomAccessory(log, config, hap) {
|
|
306
240
|
this.log = log;
|
|
241
|
+
this.hap = hap;
|
|
307
242
|
|
|
308
243
|
this.zone_number = zpad(config["zone_number"] || config["area_number"], 3);
|
|
309
244
|
this.name = config["name"];
|
|
310
245
|
this.zone_type = config["zone_type"] || config["area_type"] || "motion";
|
|
311
246
|
this.dwell_time = config["dwell"] || 0;
|
|
247
|
+
this.dwell_timer = null;
|
|
312
248
|
|
|
313
249
|
if (config["area_type"] == "securitysystem") {
|
|
314
|
-
this.target_State = Characteristic.SecuritySystemTargetState.DISARM;
|
|
250
|
+
this.target_State = hap.Characteristic.SecuritySystemTargetState.DISARM;
|
|
315
251
|
}
|
|
252
|
+
|
|
316
253
|
try {
|
|
317
254
|
if (Array.isArray(config["zones"])) {
|
|
318
|
-
// If it's an array, apply zpad to each element
|
|
319
255
|
this.zones = config["zones"].map(zone => zpad(zone, 3));
|
|
320
256
|
} else {
|
|
321
|
-
// Otherwise, treat it as a single value
|
|
322
257
|
this.zones = zpad(config["zones"], 3);
|
|
323
258
|
}
|
|
324
|
-
} catch (e) {
|
|
325
|
-
//log.error('Error processing zones: ', e);
|
|
326
|
-
}
|
|
259
|
+
} catch (e) { /* no zones */ }
|
|
327
260
|
|
|
328
261
|
if (config["sn"]) {
|
|
329
262
|
this.sn = config["sn"];
|
|
330
263
|
} else {
|
|
331
|
-
|
|
332
|
-
shasum.update(this.zone_number
|
|
333
|
-
|
|
264
|
+
const shasum = crypto.createHash('sha1');
|
|
265
|
+
shasum.update(this.zone_number);
|
|
334
266
|
this.sn = shasum.digest('base64');
|
|
335
267
|
log.log(`Computed SN: ${this.sn}`);
|
|
336
268
|
}
|
|
@@ -339,212 +271,183 @@ function TexecomAccessory(log, config) {
|
|
|
339
271
|
TexecomAccessory.prototype = {
|
|
340
272
|
|
|
341
273
|
getServices: function () {
|
|
342
|
-
|
|
274
|
+
const { Service, Characteristic } = this.hap;
|
|
343
275
|
const me = this;
|
|
344
276
|
|
|
345
277
|
var service, changeAction;
|
|
346
278
|
|
|
279
|
+
// informationService is returned but the platform populates the
|
|
280
|
+
// built-in one on the platformAccessory; this keeps the shape identical
|
|
347
281
|
var informationService = new Service.AccessoryInformation();
|
|
348
|
-
|
|
349
282
|
informationService
|
|
350
|
-
.setCharacteristic(Characteristic.Name, this.name)
|
|
351
283
|
.setCharacteristic(Characteristic.Manufacturer, "Homebridge")
|
|
352
|
-
.setCharacteristic(Characteristic.Model, `Texecom ${
|
|
284
|
+
.setCharacteristic(Characteristic.Model, `Texecom ${this.zone_type === "securitysystem" ? "Area" : "Zone"}`)
|
|
353
285
|
.setCharacteristic(Characteristic.SerialNumber, this.sn);
|
|
354
286
|
|
|
355
|
-
|
|
356
287
|
switch (this.zone_type) {
|
|
357
288
|
case "contact":
|
|
358
289
|
service = new Service.ContactSensor();
|
|
359
290
|
changeAction = function (newState) {
|
|
360
291
|
service.getCharacteristic(Characteristic.ContactSensorState)
|
|
361
|
-
.
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
case "motion":
|
|
365
|
-
service = new Service.MotionSensor();
|
|
366
|
-
changeAction = function (newState) {
|
|
367
|
-
service.getCharacteristic(Characteristic.MotionDetected)
|
|
368
|
-
.setValue(newState);
|
|
292
|
+
.updateValue(newState
|
|
293
|
+
? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
|
|
294
|
+
: Characteristic.ContactSensorState.CONTACT_DETECTED);
|
|
369
295
|
};
|
|
370
296
|
break;
|
|
297
|
+
|
|
371
298
|
case "smoke":
|
|
372
299
|
service = new Service.SmokeSensor();
|
|
373
300
|
changeAction = function (newState) {
|
|
374
301
|
service.getCharacteristic(Characteristic.SmokeDetected)
|
|
375
|
-
.
|
|
302
|
+
.updateValue(newState
|
|
303
|
+
? Characteristic.SmokeDetected.SMOKE_DETECTED
|
|
304
|
+
: Characteristic.SmokeDetected.SMOKE_NOT_DETECTED);
|
|
376
305
|
};
|
|
377
306
|
break;
|
|
307
|
+
|
|
378
308
|
case "carbonmonoxide":
|
|
379
309
|
service = new Service.CarbonMonoxideSensor();
|
|
380
310
|
changeAction = function (newState) {
|
|
381
311
|
service.getCharacteristic(Characteristic.CarbonMonoxideDetected)
|
|
382
|
-
.
|
|
312
|
+
.updateValue(newState
|
|
313
|
+
? Characteristic.CarbonMonoxideDetected.CO_LEVELS_ABNORMAL
|
|
314
|
+
: Characteristic.CarbonMonoxideDetected.CO_LEVELS_NORMAL);
|
|
383
315
|
};
|
|
384
316
|
break;
|
|
385
317
|
|
|
386
318
|
case "securitysystem":
|
|
387
319
|
service = new Service.SecuritySystem();
|
|
388
320
|
|
|
389
|
-
changeAction = function (newState
|
|
321
|
+
changeAction = function (newState) {
|
|
390
322
|
var targetState;
|
|
391
323
|
switch (newState) {
|
|
392
324
|
case Characteristic.SecuritySystemCurrentState.NIGHT_ARM:
|
|
393
|
-
targetState = Characteristic.SecuritySystemTargetState.NIGHT_ARM;
|
|
394
|
-
break;
|
|
325
|
+
targetState = Characteristic.SecuritySystemTargetState.NIGHT_ARM; break;
|
|
395
326
|
case Characteristic.SecuritySystemCurrentState.AWAY_ARM:
|
|
396
|
-
targetState = Characteristic.SecuritySystemTargetState.AWAY_ARM;
|
|
397
|
-
break;
|
|
327
|
+
targetState = Characteristic.SecuritySystemTargetState.AWAY_ARM; break;
|
|
398
328
|
case Characteristic.SecuritySystemCurrentState.STAY_ARM:
|
|
399
|
-
targetState = Characteristic.SecuritySystemTargetState.STAY_ARM;
|
|
400
|
-
break;
|
|
329
|
+
targetState = Characteristic.SecuritySystemTargetState.STAY_ARM; break;
|
|
401
330
|
case Characteristic.SecuritySystemCurrentState.DISARMED:
|
|
402
|
-
targetState = Characteristic.SecuritySystemTargetState.DISARM;
|
|
403
|
-
break;
|
|
331
|
+
targetState = Characteristic.SecuritySystemTargetState.DISARM; break;
|
|
404
332
|
default:
|
|
405
|
-
targetState = null;
|
|
406
|
-
break;
|
|
333
|
+
targetState = null; break;
|
|
407
334
|
}
|
|
408
|
-
service.getCharacteristic(Characteristic.SecuritySystemCurrentState).setValue(newState);
|
|
409
|
-
|
|
410
335
|
if (targetState != null) {
|
|
411
|
-
service.getCharacteristic(Characteristic.SecuritySystemTargetState).setValue(targetState);
|
|
412
336
|
service.getCharacteristic(Characteristic.SecuritySystemTargetState).updateValue(targetState);
|
|
413
337
|
}
|
|
414
338
|
service.getCharacteristic(Characteristic.SecuritySystemCurrentState).updateValue(newState);
|
|
415
|
-
me.log.debug(`Set target state ${targetState} and current state ${newState}
|
|
339
|
+
me.log.debug(`Set target state ${targetState} and current state ${newState}`);
|
|
416
340
|
};
|
|
417
341
|
|
|
418
|
-
//
|
|
342
|
+
// Safe default on startup
|
|
419
343
|
changeAction(Characteristic.SecuritySystemCurrentState.DISARMED);
|
|
344
|
+
|
|
420
345
|
var area = this;
|
|
421
346
|
|
|
347
|
+
// v2: onSet returns a Promise; deprecated on('set', cb) removed
|
|
422
348
|
service.getCharacteristic(Characteristic.SecuritySystemTargetState)
|
|
423
|
-
.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
platform
|
|
433
|
-
|
|
434
|
-
|
|
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
|
+
});
|
|
435
367
|
});
|
|
436
368
|
break;
|
|
437
369
|
|
|
438
|
-
|
|
439
|
-
default:
|
|
370
|
+
default: // motion (and fallback)
|
|
440
371
|
service = new Service.MotionSensor();
|
|
441
372
|
changeAction = function (newState) {
|
|
442
|
-
service.getCharacteristic(Characteristic.MotionDetected)
|
|
443
|
-
.setValue(newState);
|
|
373
|
+
service.getCharacteristic(Characteristic.MotionDetected).updateValue(newState);
|
|
444
374
|
};
|
|
445
375
|
break;
|
|
446
376
|
}
|
|
447
377
|
|
|
448
|
-
this.changeHandler = function (status
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
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);
|
|
454
383
|
} else {
|
|
455
|
-
if (
|
|
456
|
-
clearTimeout(this.dwell_timer);
|
|
457
|
-
}
|
|
384
|
+
if (me.dwell_timer) clearTimeout(me.dwell_timer);
|
|
458
385
|
changeAction(newState);
|
|
459
386
|
}
|
|
460
|
-
|
|
461
|
-
if (user == "unknown") {
|
|
462
|
-
platform.log.debug(`Changing state with changeHandler to ${newState}`);
|
|
463
|
-
}
|
|
464
|
-
else {
|
|
465
|
-
platform.log.debug(`Changing state with changeHandler to ${newState} by User ${user}`);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
}.bind(this);
|
|
387
|
+
};
|
|
469
388
|
|
|
470
389
|
return [informationService, service];
|
|
471
390
|
}
|
|
472
391
|
};
|
|
473
392
|
|
|
393
|
+
// ─── Arm/disarm command ───────────────────────────────────────────────────────
|
|
474
394
|
function areaTargetSecurityStateSet(platform, accessory, service, value, callback) {
|
|
395
|
+
const { Characteristic } = platform.hap;
|
|
475
396
|
|
|
476
397
|
const hexMapping = {
|
|
477
|
-
'1': 0x01,
|
|
478
|
-
'
|
|
479
|
-
'3': 0x04,
|
|
480
|
-
'4': 0x08,
|
|
481
|
-
'5': 0x10,
|
|
482
|
-
'6': 0x20,
|
|
483
|
-
'7': 0x40,
|
|
484
|
-
'8': 0x80
|
|
398
|
+
'1': 0x01, '2': 0x02, '3': 0x04, '4': 0x08,
|
|
399
|
+
'5': 0x10, '6': 0x20, '7': 0x40, '8': 0x80
|
|
485
400
|
};
|
|
486
401
|
|
|
487
402
|
var area_number = String.fromCharCode(parseInt(hexMapping[parseInt(accessory.zone_number, 10)], 16));
|
|
488
403
|
|
|
489
|
-
|
|
490
404
|
var command;
|
|
491
405
|
switch (value) {
|
|
492
406
|
case Characteristic.SecuritySystemTargetState.NIGHT_ARM:
|
|
493
407
|
case Characteristic.SecuritySystemTargetState.STAY_ARM:
|
|
494
|
-
command = `Y${area_number}`;
|
|
495
|
-
break;
|
|
408
|
+
command = `Y${area_number}`; break;
|
|
496
409
|
case Characteristic.SecuritySystemTargetState.AWAY_ARM:
|
|
497
|
-
command = `A${area_number}`;
|
|
498
|
-
break;
|
|
410
|
+
command = `A${area_number}`; break;
|
|
499
411
|
case Characteristic.SecuritySystemTargetState.DISARM:
|
|
500
|
-
command = `D${area_number}`;
|
|
501
|
-
break;
|
|
412
|
+
command = `D${area_number}`; break;
|
|
502
413
|
default:
|
|
503
|
-
|
|
414
|
+
platform.log.debug(`Unknown target state: ${value}`);
|
|
504
415
|
callback(new Error("Unknown target state"));
|
|
505
416
|
return;
|
|
506
417
|
}
|
|
507
418
|
|
|
508
|
-
platform.log.debug(`Sending
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
419
|
+
platform.log.debug(`Sending command ${value} to area ${accessory.zone_number}`);
|
|
512
420
|
|
|
513
421
|
writeCommandAndWaitForOK(platform.texecomConnection, `W${platform.udl}`)
|
|
514
422
|
.then(() => writeCommandAndWaitForOK(platform.texecomConnection, command, 0))
|
|
515
423
|
.then(() => {
|
|
516
|
-
// OK response from alarm is only indication that the target state has been reached
|
|
517
424
|
var currentState;
|
|
518
425
|
switch (value) {
|
|
519
426
|
case Characteristic.SecuritySystemTargetState.NIGHT_ARM:
|
|
520
|
-
currentState = Characteristic.SecuritySystemCurrentState.NIGHT_ARM;
|
|
521
|
-
break;
|
|
427
|
+
currentState = Characteristic.SecuritySystemCurrentState.NIGHT_ARM; break;
|
|
522
428
|
case Characteristic.SecuritySystemTargetState.AWAY_ARM:
|
|
523
|
-
currentState = Characteristic.SecuritySystemCurrentState.AWAY_ARM;
|
|
524
|
-
break;
|
|
429
|
+
currentState = Characteristic.SecuritySystemCurrentState.AWAY_ARM; break;
|
|
525
430
|
case Characteristic.SecuritySystemTargetState.STAY_ARM:
|
|
526
|
-
currentState = Characteristic.SecuritySystemCurrentState.STAY_ARM;
|
|
527
|
-
break;
|
|
431
|
+
currentState = Characteristic.SecuritySystemCurrentState.STAY_ARM; break;
|
|
528
432
|
case Characteristic.SecuritySystemTargetState.DISARM:
|
|
529
|
-
currentState = Characteristic.SecuritySystemCurrentState.DISARMED;
|
|
530
|
-
break;
|
|
433
|
+
currentState = Characteristic.SecuritySystemCurrentState.DISARMED; break;
|
|
531
434
|
default:
|
|
532
435
|
platform.log.debug(`Unknown target alarm state ${value}`);
|
|
533
436
|
callback(new Error("Unknown target state"));
|
|
534
437
|
return;
|
|
535
438
|
}
|
|
536
|
-
platform.log.debug(`
|
|
537
|
-
service.getCharacteristic(Characteristic.SecuritySystemCurrentState)
|
|
538
|
-
.updateValue(currentState);
|
|
439
|
+
platform.log.debug(`Area ${accessory.zone_number} → state ${currentState}`);
|
|
440
|
+
service.getCharacteristic(Characteristic.SecuritySystemCurrentState).updateValue(currentState);
|
|
539
441
|
accessory.target_State = currentState;
|
|
540
442
|
callback();
|
|
541
443
|
})
|
|
542
|
-
.catch(
|
|
543
|
-
platform.log.debug(`
|
|
544
|
-
callback(err);
|
|
444
|
+
.catch(err => {
|
|
445
|
+
platform.log.debug(`Command error: ${err}`);
|
|
446
|
+
callback(err);
|
|
545
447
|
});
|
|
546
448
|
}
|
|
547
449
|
|
|
450
|
+
// ─── Command writer ───────────────────────────────────────────────────────────
|
|
548
451
|
function writeCommandAndWaitForOK(connection, command, retryCount = 1) {
|
|
549
452
|
return new Promise((resolve, reject) => {
|
|
550
453
|
function handleData(data) {
|
|
@@ -558,18 +461,16 @@ function writeCommandAndWaitForOK(connection, command, retryCount = 1) {
|
|
|
558
461
|
|
|
559
462
|
connection.write(`\\${command}/`, function (err) {
|
|
560
463
|
if (err) {
|
|
561
|
-
|
|
464
|
+
responseEmitter.removeListener('data', handleData);
|
|
562
465
|
reject(err);
|
|
563
|
-
} else {
|
|
564
|
-
platform.log.debug(`Command sent: ${command}`);
|
|
565
466
|
}
|
|
566
467
|
});
|
|
567
468
|
|
|
568
469
|
setTimeout(() => {
|
|
569
470
|
responseEmitter.removeListener('data', handleData);
|
|
570
471
|
if (retryCount > 0) {
|
|
571
|
-
|
|
572
|
-
|
|
472
|
+
writeCommandAndWaitForOK(connection, command, retryCount - 1)
|
|
473
|
+
.then(resolve).catch(reject);
|
|
573
474
|
} else {
|
|
574
475
|
reject(new Error("Timeout after retries"));
|
|
575
476
|
}
|
|
@@ -577,8 +478,7 @@ function writeCommandAndWaitForOK(connection, command, retryCount = 1) {
|
|
|
577
478
|
});
|
|
578
479
|
}
|
|
579
480
|
|
|
481
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
580
482
|
function is_armed(area_number) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
return isAreaArmed;
|
|
483
|
+
return areas_armed.some(v => v === area_number);
|
|
584
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
|
+
}
|