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.
Files changed (3) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/index.js +190 -290
  3. package/package.json +7 -6
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  This change log documents all release versions of homebridge-texecom
4
4
 
5
+ ### 4.2.8 (2025-7-28)
6
+
7
+ - **TWEAK** - Data conversion issue (Typo)
8
+ - **NEWS** - We are verified!!
9
+
5
10
  ### 4.2.7 (2025-7-28)
6
11
 
7
12
  - **FIX** - Connection issue
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;
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
- homebridge.registerAccessory("homebridge-texecom-full", "Texecom", TexecomAccessory);
29
- homebridge.registerPlatform("homebridge-texecom-full", "Texecom", TexecomPlatform);
30
- }
42
+ // Required by v2 — called for each cached accessory; we don't cache, so no-op
43
+ configureAccessory(accessory) { }
31
44
 
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
- }
45
+ _setupAccessories() {
46
+ const platform = this;
47
+ const { hap, api } = this;
47
48
 
48
- TexecomPlatform.prototype = {
49
-
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,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
- if (this.serial_device) {
206
- var SerialPort = serialport.SerialPort;
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
- serialPort.on("open", function () {
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
- connection.on('error', function (err) {
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('end', function () {
249
- platform.log.log('IP connection ended');
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('close', function () {
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
- connection = net.createConnection(platform.ip_port, platform.ip_address);
206
+ platform.texecomConnection = connection;
207
+ }
273
208
 
274
- connection.setNoDelay(true);
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
- connection.on('connect', () => {
277
- platform.log.log('Connected via IP');
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
- connection.on('data', function (data) {
281
- platform.log.debug(`IP data received: ${data}`);
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
- connection.on('error', (err) => {
287
- platform.log.error('IP connection error:', err.message);
288
- // We'll reconnect on 'close'
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
- connection.on('close', () => {
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
- connection.on('end', () => {
299
- platform.log.log('IP connection ended');
300
- });
229
+ } else if (this.ip_address) {
230
+ setupConnection();
301
231
 
302
- platform.texecomConnection = connection;
232
+ } else {
233
+ this.log.log("Must set either serial_device or ip_address in configuration.");
234
+ }
235
+ }
303
236
  }
304
237
 
305
- function TexecomAccessory(log, config) {
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
- var shasum = crypto.createHash('sha1');
332
- shasum.update(this.zone_number/* || this.area_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 ${(this.accessoryType === "zone" ? "Zone" : "Area")}`)
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
- .setValue(newState ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED);
362
- };
363
- break;
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
- .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);
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
- .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);
383
315
  };
384
316
  break;
385
317
 
386
318
  case "securitysystem":
387
319
  service = new Service.SecuritySystem();
388
320
 
389
- changeAction = function (newState, user = "unknown") {
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; // alarm triggered has no corresponding target state
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} in response to notification from alarm`);
339
+ me.log.debug(`Set target state ${targetState} and current state ${newState}`);
416
340
  };
417
341
 
418
- // we don't know the alarm's state at startup, safer to assume disarmed:
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
- .on('set', function (value, callback) {
424
- if (setByAlarm) {
425
- 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.`);
426
- setByAlarm = false;
427
- return;
428
- }
429
- if (platform.udl != null) {
430
- areaTargetSecurityStateSet(platform, area, service, value, callback);
431
- } else {
432
- platform.log.debug("No UDL configured. Add your UDL to enable arm/disarm from HomeKit.");
433
- callback(new Error("No UDL configured"));
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, user = "unknown") {
449
- var newState = status;
450
- platform.log.debug(`Dwell = ${this.dwell_time}`);
451
-
452
- if (!newState && this.dwell_time > 0) {
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 (this.dwell_timer) {
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
- '2': 0x02,
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}`; // Home
495
- break;
408
+ command = `Y${area_number}`; break;
496
409
  case Characteristic.SecuritySystemTargetState.AWAY_ARM:
497
- command = `A${area_number}`; // Away arm
498
- break;
410
+ command = `A${area_number}`; break;
499
411
  case Characteristic.SecuritySystemTargetState.DISARM:
500
- command = `D${area_number}`; // Disarm
501
- break;
412
+ command = `D${area_number}`; break;
502
413
  default:
503
- this.log.debug(`Unknown target state: ${value}`);
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 arm/disarm command ${value} to area ${accessory.zone_number}`);
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(`Setting current status of area ${accessory.zone_number} to ${currentState} because alarm responded OK`);
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((err) => {
543
- platform.log.debug(`Callback with error ${err}`);
544
- callback(err); // Handle errors
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
- platform.log.debug(`Error writing to connection: ${err}`);
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
- platform.log.debug(`Retrying command due to timeout, retries left: ${retryCount}`);
572
- writeCommandAndWaitForOK(connection, command, retryCount - 1).then(resolve).catch(reject);
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
- let isAreaArmed = areas_armed.some(value => value === area_number);
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.2.7",
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
+ }