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.
Files changed (2) hide show
  1. package/index.js +174 -275
  2. 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 = []; // Creates an empty array, which can later grow dynamically
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
- module.exports = function (homebridge) {
24
- Accessory = homebridge.platformAccessory;
25
- Service = homebridge.hap.Service;
26
- Characteristic = homebridge.hap.Characteristic;
27
-
28
- homebridge.registerAccessory("homebridge-texecom-full", "Texecom", TexecomAccessory);
29
- homebridge.registerPlatform("homebridge-texecom-full", "Texecom", TexecomPlatform);
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
- function TexecomPlatform(log, config) {
33
- this.log = new LogUtil(
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
- TexecomPlatform.prototype = {
45
+ _setupAccessories() {
46
+ const platform = this;
47
+ const { hap, api } = this;
49
48
 
50
- accessories: function (callback) {
51
- var zoneAccessories = [];
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
- var areaAccessories = [];
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
- callback(zoneAccessories.concat(areaAccessories));
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
- platform = this;
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
- catch (e) {
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
- // Extract the area number that is being updated
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(value => value !== zpad(updated_area, 3));
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
- platform.log.log(`User ${user}: Target state is ${targetState}, updated stateValue is ${stateValue}`);
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
- // Find the index of the first null or undefined value
177
- const index = areas_armed.findIndex(value => value === null || value === undefined);
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. Will attempt to reconnect in 10s.');
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
- var SerialPort = serialport.SerialPort;
243
-
244
- serialPort = new SerialPort(this.serial_device, {
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
- setupConnection();
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
- /*try {
264
- connection = net.createConnection(platform.ip_port, platform.ip_address, function () {
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
- connection.on('error', function (err) {
280
- platform.log.error(`IP connection error: ${err}`);
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
- var shasum = crypto.createHash('sha1');
333
- shasum.update(this.zone_number/* || this.area_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 ${(this.accessoryType === "zone" ? "Zone" : "Area")}`)
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
- .setValue(newState ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED);
363
- };
364
- break;
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
- .setValue(newState ? Characteristic.ContactSensorState.SMOKE_DETECTED : Characteristic.ContactSensorState.SMOKE_NOT_DETECTED);
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
- .setValue(newState ? Characteristic.CarbonMonoxideDetected.CO_LEVELS_ABNORMAL : Characteristic.CarbonMonoxideDetected.CO_LEVELS_NORMAL);
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, user = "unknown") {
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; // alarm triggered has no corresponding target state
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} in response to notification from alarm`);
339
+ me.log.debug(`Set target state ${targetState} and current state ${newState}`);
417
340
  };
418
341
 
419
- // we don't know the alarm's state at startup, safer to assume disarmed:
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
- .on('set', function (value, callback) {
425
- if (setByAlarm) {
426
- platform.log.debug(`Not sending command to alarm for change to state ${value} because the state change appears to have come from the alarm itself.`);
427
- setByAlarm = false;
428
- return;
429
- }
430
- if (platform.udl != null) {
431
- areaTargetSecurityStateSet(platform, area, service, value, callback);
432
- } else {
433
- platform.log.debug("No UDL configured. Add your UDL to enable arm/disarm from HomeKit.");
434
- callback(new Error("No UDL configured"));
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, user = "unknown") {
450
- var newState = status;
451
- platform.log.debug(`Dwell = ${this.dwell_time}`);
452
-
453
- if (!newState && this.dwell_time > 0) {
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 (this.dwell_timer) {
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
- '2': 0x02,
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}`; // Home
496
- break;
408
+ command = `Y${area_number}`; break;
497
409
  case Characteristic.SecuritySystemTargetState.AWAY_ARM:
498
- command = `A${area_number}`; // Away arm
499
- break;
410
+ command = `A${area_number}`; break;
500
411
  case Characteristic.SecuritySystemTargetState.DISARM:
501
- command = `D${area_number}`; // Disarm
502
- break;
412
+ command = `D${area_number}`; break;
503
413
  default:
504
- this.log.debug(`Unknown target state: ${value}`);
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 arm/disarm command ${value} to area ${accessory.zone_number}`);
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(`Setting current status of area ${accessory.zone_number} to ${currentState} because alarm responded OK`);
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((err) => {
544
- platform.log.debug(`Callback with error ${err}`);
545
- callback(err); // Handle errors
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
- platform.log.debug(`Error writing to connection: ${err}`);
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
- platform.log.debug(`Retrying command due to timeout, retries left: ${retryCount}`);
573
- writeCommandAndWaitForOK(connection, command, retryCount - 1).then(resolve).catch(reject);
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
- let isAreaArmed = areas_armed.some(value => value === area_number);
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.2.8",
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://https://github.com/K1LL3R234/homebridge-texecom/issues"
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": "^1.6.0 || ^2.0.0-beta.0",
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
+ }