homebridge-texecom-full 4.2.8 → 4.3.0-beta.1

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