homebridge-unifi-access 1.9.2 → 1.10.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.
@@ -1,6 +1,51 @@
1
- import { acquireService, validService } from "homebridge-plugin-utils";
1
+ /* Copyright(C) 2019-2025, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * access-hub.ts: Unified hub and reader device class for UniFi Access.
4
+ */
2
5
  import { AccessDevice } from "./access-device.js";
6
+ import { acquireService, validService } from "homebridge-plugin-utils";
3
7
  import { AccessReservedNames } from "./access-types.js";
8
+ // Access methods available to us for readers.
9
+ const accessMethods = [
10
+ { capability: "identity_face_unlock", key: "face", name: "Face Unlock", option: "AccessMethod.Face", subtype: AccessReservedNames.SWITCH_ACCESSMETHOD_FACE },
11
+ { capability: "hand_wave", key: "wave", name: "Hand Wave", option: "AccessMethod.Hand", subtype: AccessReservedNames.SWITCH_ACCESSMETHOD_HAND },
12
+ { capability: "mobile_unlock_ver2", key: "bt_button", name: "Mobile", option: "AccessMethod.Mobile", subtype: AccessReservedNames.SWITCH_ACCESSMETHOD_MOBILE },
13
+ { capability: "nfc_card_easy_provision", key: "nfc", name: "NFC", option: "AccessMethod.NFC", subtype: AccessReservedNames.SWITCH_ACCESSMETHOD_NFC },
14
+ { capability: "pin_code", key: "pin_code", name: "PIN", option: "AccessMethod.PIN", subtype: AccessReservedNames.SWITCH_ACCESSMETHOD_PIN },
15
+ { capability: "qr_code", key: "qr_code", name: "QR Code", option: "AccessMethod.QR", subtype: AccessReservedNames.SWITCH_ACCESSMETHOD_QR }
16
+ ];
17
+ // Define the dry contact inputs we're interested in for Access hubs.
18
+ const sensorInputs = ["Dps", "Rel", "Ren", "Rex"];
19
+ // Define the sensor wiring. It's a bit convoluted because there's a lot of inconsistency at the API level across device types in Access:
20
+ // - For UA-ULTRA, we look at rex_button_mode = proxyMode.
21
+ // - For other models, we look at per-device wiring keys.
22
+ const sensorWiring = {
23
+ Dps: {
24
+ proxyMode: "dps",
25
+ wiring: {
26
+ "UA-Hub-Door-Mini": ["wiring_state_d1-dps-neg", "wiring_state_d1-dps-pos"],
27
+ UAH: ["wiring_state_dps-neg", "wiring_state_dps-pos"],
28
+ UGT: ["wiring_state_gate-dps-neg", "wiring_state_gate-dps-pos"]
29
+ }
30
+ },
31
+ Rel: {
32
+ wiring: {
33
+ UAH: ["wiring_state_rel-neg", "wiring_state_rel-pos"]
34
+ }
35
+ },
36
+ Ren: {
37
+ wiring: {
38
+ UAH: ["wiring_state_ren-neg", "wiring_state_ren-pos"]
39
+ }
40
+ },
41
+ Rex: {
42
+ proxyMode: "rex",
43
+ wiring: {
44
+ "UA-Hub-Door-Mini": ["wiring_state_d1-button-neg", "wiring_state_d1-button-pos"],
45
+ UAH: ["wiring_state_rex-neg", "wiring_state_rex-pos"]
46
+ }
47
+ }
48
+ };
4
49
  export class AccessHub extends AccessDevice {
5
50
  _hkLockState;
6
51
  doorbellRingRequestId;
@@ -24,10 +69,21 @@ export class AccessHub extends AccessDevice {
24
69
  configureHints() {
25
70
  // Configure our parent's hints.
26
71
  super.configureHints();
27
- this.hints.hasDps = this.hasCapability(["dps_alarm", "dps_mode_selectable", "dps_trigger_level"]) && this.hasFeature("Hub.DPS");
72
+ this.hints.hasWiringDps = ["UA Ultra", "UA Hub", "UA Hub Door Mini"].includes(this.uda.display_model ?? "") && this.hasFeature("Hub.DPS");
73
+ this.hints.hasWiringRel = ["UA Hub"].includes(this.uda.display_model ?? "") && this.hasFeature("Hub.REL");
74
+ this.hints.hasWiringRen = ["UA Hub"].includes(this.uda.display_model ?? "") && this.hasFeature("Hub.REN");
75
+ this.hints.hasWiringRex = ["UA Ultra", "UA Hub", "UA Hub Door Mini"].includes(this.uda.display_model ?? "") && this.hasFeature("Hub.REX");
28
76
  this.hints.logDoorbell = this.hasFeature("Log.Doorbell");
29
77
  this.hints.logDps = this.hasFeature("Log.DPS");
30
78
  this.hints.logLock = this.hasFeature("Log.Lock");
79
+ this.hints.logRel = this.hasFeature("Log.REL");
80
+ this.hints.logRen = this.hasFeature("Log.REN");
81
+ this.hints.logRex = this.hasFeature("Log.REX");
82
+ // The Ultra has a single terminal input that's selectable between DPS and REX modes. We detect which mode it's operating in, and adjust accordingly. We've
83
+ // over-engineered this a bit for future-proofing.
84
+ if (this.uda.display_model === "UA Ultra") {
85
+ this.checkUltraInputs();
86
+ }
31
87
  return true;
32
88
  }
33
89
  // Initialize and configure the light accessory for HomeKit.
@@ -45,14 +101,16 @@ export class AccessHub extends AccessDevice {
45
101
  }
46
102
  // Configure accessory information.
47
103
  this.configureInfo();
48
- // Configure the lock.
104
+ // Configure access method switches, if we're a reader device.
105
+ this.configureAccessMethodSwitches();
106
+ // Configure the lock, if we're a hub device.
49
107
  this.configureLock();
50
108
  this.configureLockTrigger();
51
- // Configure the doorbell.
109
+ // Configure the doorbell, if we have one.
52
110
  this.configureDoorbell();
53
111
  this.configureDoorbellTrigger();
54
- // Configure the door position sensor.
55
- this.configureDps();
112
+ // Configure the sensors connected to terminal inputs.
113
+ this.configureTerminalInputs();
56
114
  // Configure MQTT services.
57
115
  this.configureMqtt();
58
116
  // Listen for events.
@@ -61,6 +119,43 @@ export class AccessHub extends AccessDevice {
61
119
  this.controller.events.on("access.remote_view.change", this.listeners["access.remote_view.change"] = this.eventHandler.bind(this));
62
120
  return true;
63
121
  }
122
+ // Configure the access method switches for HomeKit.
123
+ configureAccessMethodSwitches() {
124
+ for (const accessMethod of accessMethods) {
125
+ // Validate whether we should have this service enabled.
126
+ if (!validService(this.accessory, this.hap.Service.Switch, this.hasCapability("is_reader") && this.hasCapability(accessMethod.capability) && this.hasFeature(accessMethod.option), accessMethod.subtype)) {
127
+ continue;
128
+ }
129
+ // Acquire the service.
130
+ const service = acquireService(this.accessory, this.hap.Service.Switch, this.accessoryName + " " + accessMethod.name, accessMethod.subtype);
131
+ if (!service) {
132
+ this.log.error("Unable to add the %s access method switch.", accessMethod.name);
133
+ continue;
134
+ }
135
+ // Retrieve the state when requested.
136
+ service.getCharacteristic(this.hap.Characteristic.On).onGet(() => Boolean(this.uda.configs?.find(entry => entry.key === accessMethod.key)?.value === "yes"));
137
+ // Set the state when requested.
138
+ service.getCharacteristic(this.hap.Characteristic.On).onSet(async (value) => {
139
+ const entry = this.uda.configs?.find(entry => entry.key === accessMethod.key);
140
+ let success;
141
+ if (entry) {
142
+ const response = await this.controller.udaApi.retrieve(this.controller.udaApi.getApiEndpoint("device") + "/" + this.id + "/settings", {
143
+ body: JSON.stringify([{ key: entry.key, tag: "open_door_mode", value: value ? "yes" : "no" }]),
144
+ method: "PUT"
145
+ });
146
+ success = this.controller.udaApi.responseOk(response?.statusCode);
147
+ }
148
+ // If we didn't find the configuration entry or we didn't succeed in setting the value, revert our switch state.
149
+ if (!success) {
150
+ this.log.error("Unable to %s the %s access method.", value ? "activate" : "deactivate", accessMethod.name);
151
+ setTimeout(() => service.updateCharacteristic(this.hap.Characteristic.On, !value), 50);
152
+ }
153
+ });
154
+ // Initialize the switch.
155
+ service.updateCharacteristic(this.hap.Characteristic.On, Boolean(this.uda.configs?.find(entry => entry.key === accessMethod.key)?.value === "yes"));
156
+ }
157
+ return true;
158
+ }
64
159
  // Configure the doorbell service for HomeKit.
65
160
  configureDoorbell() {
66
161
  // Validate whether we should have this service enabled.
@@ -68,7 +163,7 @@ export class AccessHub extends AccessDevice {
68
163
  return false;
69
164
  }
70
165
  // Acquire the service.
71
- const service = acquireService(this.hap, this.accessory, this.hap.Service.Doorbell, this.accessoryName, undefined, () => this.log.info("Enabling the doorbell."));
166
+ const service = acquireService(this.accessory, this.hap.Service.Doorbell, this.accessoryName, undefined, () => this.log.info("Enabling the doorbell."));
72
167
  if (!service) {
73
168
  this.log.error("Unable to add the doorbell.");
74
169
  return false;
@@ -76,33 +171,62 @@ export class AccessHub extends AccessDevice {
76
171
  service.setPrimaryService(true);
77
172
  return true;
78
173
  }
79
- // Configure the door position sensor for HomeKit.
80
- configureDps() {
81
- // Validate whether we should have this service enabled.
82
- if (!validService(this.accessory, this.hap.Service.ContactSensor, this.hints.hasDps, AccessReservedNames.CONTACT_DPS)) {
83
- return false;
84
- }
85
- // Acquire the service.
86
- const service = acquireService(this.hap, this.accessory, this.hap.Service.ContactSensor, this.accessoryName + " Door Position Sensor", AccessReservedNames.CONTACT_DPS, () => this.log.info("Enabling the door position sensor."));
87
- if (!service) {
88
- this.log.error("Unable to add the door position sensor.");
89
- return false;
174
+ // Configure our contact sensors for HomeKit. Availability is determined by a combination of hub model, what's been configured on the hub, and feature options.
175
+ configureTerminalInputs() {
176
+ const terminalInputs = [
177
+ { input: "Dps", label: "Door Position Sensor" },
178
+ { input: "Rel", label: "Remote Release" },
179
+ { input: "Ren", label: "Request to Enter Sensor" },
180
+ { input: "Rex", label: "Request to Exit Sensor" }
181
+ ];
182
+ for (const { input, label } of terminalInputs) {
183
+ const hint = ("hasWiring" + input);
184
+ const reservedId = AccessReservedNames[("CONTACT_" + input.toUpperCase())];
185
+ const state = ("hub" + input + "State");
186
+ // Validate whether we should have this service enabled.
187
+ if (!validService(this.accessory, this.hap.Service.ContactSensor, (hasService) => {
188
+ if (!this.hints[hint] && hasService) {
189
+ this.log.info("Disabling the " + label.toLowerCase() + ".");
190
+ }
191
+ return this.hints[hint];
192
+ }, reservedId)) {
193
+ continue;
194
+ }
195
+ // Acquire the service.
196
+ const service = acquireService(this.accessory, this.hap.Service.ContactSensor, this.accessoryName + " " + label, reservedId, () => this.log.info("Enabling the " + label.toLowerCase() + "."));
197
+ if (!service) {
198
+ this.log.error("Unable to add the " + label.toLowerCase() + ".");
199
+ continue;
200
+ }
201
+ // Initialize the sensor state.
202
+ service.updateCharacteristic(this.hap.Characteristic.ContactSensorState, this[state]);
203
+ service.updateCharacteristic(this.hap.Characteristic.StatusActive, !!this.uda.is_online);
204
+ // If the hub has tamper indicator capabilities, let's reflect that in HomeKit.
205
+ if (this.hasCapability("tamper_proofing")) {
206
+ const tamperedEntry = this.uda.configs?.find(entry => entry.key === "tamper_event");
207
+ if (tamperedEntry) {
208
+ service.updateCharacteristic(this.hap.Characteristic.StatusTampered, (tamperedEntry.value === "true") ? this.hap.Characteristic.StatusTampered.TAMPERED :
209
+ this.hap.Characteristic.StatusTampered.NOT_TAMPERED);
210
+ }
211
+ }
90
212
  }
91
- // Initialize the light.
92
- service.updateCharacteristic(this.hap.Characteristic.ContactSensorState, this.hubDpsState);
93
213
  return true;
94
214
  }
95
215
  // Configure the lock for HomeKit.
96
216
  configureLock() {
217
+ // Validate whether we should have this service enabled.
218
+ if (!validService(this.accessory, this.hap.Service.LockMechanism, this.hasCapability("is_hub"))) {
219
+ return false;
220
+ }
97
221
  // Acquire the service.
98
- const service = acquireService(this.hap, this.accessory, this.hap.Service.LockMechanism, this.accessoryName);
222
+ const service = acquireService(this.accessory, this.hap.Service.LockMechanism, this.accessoryName);
99
223
  if (!service) {
100
224
  this.log.error("Unable to add the lock.");
101
225
  return false;
102
226
  }
103
227
  // Return the lock state.
104
- service.getCharacteristic(this.hap.Characteristic.LockCurrentState)?.onGet(() => this.hkLockState);
105
- service.getCharacteristic(this.hap.Characteristic.LockTargetState)?.onSet(async (value) => {
228
+ service.getCharacteristic(this.hap.Characteristic.LockCurrentState).onGet(() => this.hkLockState);
229
+ service.getCharacteristic(this.hap.Characteristic.LockTargetState).onSet(async (value) => {
106
230
  if (!(await this.hubLockCommand(value === this.hap.Characteristic.LockTargetState.SECURED))) {
107
231
  // Revert our target state.
108
232
  setTimeout(() => service.updateCharacteristic(this.hap.Characteristic.LockTargetState, !value), 50);
@@ -124,15 +248,15 @@ export class AccessHub extends AccessDevice {
124
248
  return false;
125
249
  }
126
250
  // Acquire the service.
127
- const service = acquireService(this.hap, this.accessory, this.hap.Service.Switch, this.accessoryName + " Doorbell Trigger", AccessReservedNames.SWITCH_DOORBELL_TRIGGER, () => this.log.info("Enabling the doorbell automation trigger."));
251
+ const service = acquireService(this.accessory, this.hap.Service.Switch, this.accessoryName + " Doorbell Trigger", AccessReservedNames.SWITCH_DOORBELL_TRIGGER, () => this.log.info("Enabling the doorbell automation trigger."));
128
252
  if (!service) {
129
253
  this.log.error("Unable to add the doorbell automation trigger.");
130
254
  return false;
131
255
  }
132
256
  // Trigger the doorbell.
133
- service.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => this.doorbellRingRequestId !== null);
257
+ service.getCharacteristic(this.hap.Characteristic.On).onGet(() => this.doorbellRingRequestId !== null);
134
258
  // The state isn't really user-triggerable. We have no way, currently, to trigger a ring event on the hub.
135
- service.getCharacteristic(this.hap.Characteristic.On)?.onSet(() => {
259
+ service.getCharacteristic(this.hap.Characteristic.On).onSet(() => {
136
260
  setTimeout(() => service.updateCharacteristic(this.hap.Characteristic.On, this.doorbellRingRequestId !== null), 50);
137
261
  });
138
262
  // Initialize the switch.
@@ -143,19 +267,19 @@ export class AccessHub extends AccessDevice {
143
267
  // Configure a switch to automate lock and unlock events in HomeKit beyond what HomeKit might allow for a lock service that gets treated as a secure service.
144
268
  configureLockTrigger() {
145
269
  // Validate whether we should have this service enabled.
146
- if (!validService(this.accessory, this.hap.Service.Switch, this.hasFeature("Hub.Lock.Trigger"), AccessReservedNames.SWITCH_LOCK_TRIGGER)) {
270
+ if (!validService(this.accessory, this.hap.Service.Switch, this.hasCapability("is_hub") && this.hasFeature("Hub.Lock.Trigger"), AccessReservedNames.SWITCH_LOCK_TRIGGER)) {
147
271
  return false;
148
272
  }
149
273
  // Acquire the service.
150
- const service = acquireService(this.hap, this.accessory, this.hap.Service.Switch, this.accessoryName + " Lock Trigger", AccessReservedNames.SWITCH_LOCK_TRIGGER, () => this.log.info("Enabling the lock automation trigger."));
274
+ const service = acquireService(this.accessory, this.hap.Service.Switch, this.accessoryName + " Lock Trigger", AccessReservedNames.SWITCH_LOCK_TRIGGER, () => this.log.info("Enabling the lock automation trigger."));
151
275
  if (!service) {
152
276
  this.log.error("Unable to add the lock automation trigger.");
153
277
  return false;
154
278
  }
155
279
  // Trigger the doorbell.
156
- service.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => this.hkLockState !== this.hap.Characteristic.LockCurrentState.SECURED);
280
+ service.getCharacteristic(this.hap.Characteristic.On).onGet(() => this.hkLockState !== this.hap.Characteristic.LockCurrentState.SECURED);
157
281
  // The state isn't really user-triggerable. We have no way, currently, to trigger a lock or unlock event on the hub.
158
- service.getCharacteristic(this.hap.Characteristic.On)?.onSet(async (value) => {
282
+ service.getCharacteristic(this.hap.Characteristic.On).onSet(async (value) => {
159
283
  // If we are on, we are in an unlocked state. If we are off, we are in a locked state.
160
284
  if (!(await this.hubLockCommand(!value))) {
161
285
  // Revert our state.
@@ -218,6 +342,23 @@ export class AccessHub extends AccessDevice {
218
342
  });
219
343
  return true;
220
344
  }
345
+ // Check and validate Ultra inputs with what the user has configured in HomeKit.
346
+ checkUltraInputs() {
347
+ for (const input of ["Dps", "Rex"]) {
348
+ const hint = ("hasWiring" + input);
349
+ const mode = input.toLowerCase();
350
+ // Is the mode enabled on the hub?
351
+ const isEnabled = this.uda.extensions?.[0]?.target_config?.some(entry => (entry.config_key === "rex_button_mode") && entry.config_value === mode);
352
+ if (this.hints[hint] && !isEnabled) {
353
+ // The hub has disabled this input.
354
+ this.hints[hint] = false;
355
+ }
356
+ else if (!this.hints[hint] && isEnabled && this.hasFeature("Hub." + input.toUpperCase())) {
357
+ // The hub has the input enabled, and we want it enabled in HomeKit.
358
+ this.hints[hint] = true;
359
+ }
360
+ }
361
+ }
221
362
  // Utility function to execute lock and unlock actions on a hub.
222
363
  async hubLockCommand(isLocking) {
223
364
  const action = isLocking ? "lock" : "unlock";
@@ -238,16 +379,6 @@ export class AccessHub extends AccessDevice {
238
379
  }
239
380
  return true;
240
381
  }
241
- // Return the current HomeKit DPS state that we are tracking for this hub.
242
- get hkDpsState() {
243
- return this.accessory.getService(this.hap.Service.ContactSensor)?.getCharacteristic(this.hap.Characteristic.ContactSensorState).value ??
244
- this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED;
245
- }
246
- // Set the current HomeKit DPS state for this hub.
247
- set hkDpsState(value) {
248
- // Update the state of the contact service.
249
- this.accessory.getService(this.hap.Service.ContactSensor)?.updateCharacteristic(this.hap.Characteristic.ContactSensorState, value);
250
- }
251
382
  // Return the current HomeKit lock state that we are tracking for this hub.
252
383
  get hkLockState() {
253
384
  return this._hkLockState;
@@ -283,12 +414,15 @@ export class AccessHub extends AccessDevice {
283
414
  case "UA-ULTRA":
284
415
  relayType = "input_d1_dps";
285
416
  break;
417
+ case "UGT":
418
+ relayType = "input_gate_dps";
419
+ break;
286
420
  default:
287
421
  relayType = "input_state_dps";
288
422
  break;
289
423
  }
290
424
  // Return our DPS state. If it's anything other than on, we assume it's open.
291
- return (this.uda.configs?.find(x => x.key === relayType)?.value === "on") ? this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED :
425
+ return (this.uda.configs?.find(entry => entry.key === relayType)?.value === "on") ? this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED :
292
426
  this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED;
293
427
  }
294
428
  // Return the current state of the relay lock on the hub.
@@ -299,39 +433,109 @@ export class AccessHub extends AccessDevice {
299
433
  case "UA-ULTRA":
300
434
  relayType = "output_d1_lock_relay";
301
435
  break;
436
+ case "UGT":
437
+ relayType = "output_oper1_relay";
438
+ break;
302
439
  default:
303
440
  relayType = "input_state_rly-lock_dry";
304
441
  break;
305
442
  }
306
- const lockRelay = this.uda.configs?.find(x => x.key === relayType);
307
- return ((lockRelay?.value === "off") ? this.hap.Characteristic.LockCurrentState.SECURED : this.hap.Characteristic.LockCurrentState.UNSECURED) ??
308
- this.hap.Characteristic.LockCurrentState.UNKNOWN;
443
+ const lockRelay = this.uda.configs?.find(entry => entry.key === relayType);
444
+ return (lockRelay?.value === "off") ? this.hap.Characteristic.LockCurrentState.SECURED : this.hap.Characteristic.LockCurrentState.UNSECURED;
445
+ }
446
+ // Return the current state of the REL on the hub.
447
+ get hubRelState() {
448
+ // If we don't have the wiring connected for the REL, we report our default closed state.
449
+ if (!this.isRelWired) {
450
+ return this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED;
451
+ }
452
+ let relayType;
453
+ switch (this.uda.device_type) {
454
+ case "UAH":
455
+ relayType = "input_state_rel";
456
+ break;
457
+ default:
458
+ return this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED;
459
+ }
460
+ // Return our REL state. If it's anything other than on, we assume it's open.
461
+ return (this.uda.configs?.find(relay => relay.key === relayType)?.value === "on") ? this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED :
462
+ this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED;
309
463
  }
310
- // Return whether the DPS has been wired on the hub.
311
- get isDpsWired() {
312
- let wiringType = [];
464
+ // Return the current state of the REN on the hub.
465
+ get hubRenState() {
466
+ // If we don't have the wiring connected for the REN, we report our default closed state.
467
+ if (!this.isRenWired) {
468
+ return this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED;
469
+ }
470
+ let relayType;
471
+ switch (this.uda.device_type) {
472
+ case "UAH":
473
+ relayType = "input_state_ren";
474
+ break;
475
+ default:
476
+ return this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED;
477
+ }
478
+ // Return our REN state. If it's anything other than on, we assume it's open.
479
+ return (this.uda.configs?.find(relay => relay.key === relayType)?.value === "on") ? this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED :
480
+ this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED;
481
+ }
482
+ // Return the current state of the REX on the hub.
483
+ get hubRexState() {
484
+ // If we don't have the wiring connected for the REX, we report our default closed state.
485
+ if (!this.isRexWired) {
486
+ return this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED;
487
+ }
488
+ let relayType;
313
489
  switch (this.uda.device_type) {
314
490
  case "UA-Hub-Door-Mini":
315
- wiringType = ["wiring_state_d1-dps-neg", "wiring_state_d1-dps-pos"];
491
+ case "UA-ULTRA":
492
+ relayType = "input_d1_button";
316
493
  break;
317
494
  case "UAH":
318
- wiringType = ["wiring_state_dps-neg", "wiring_state_dps-pos"];
495
+ relayType = "input_state_rex";
319
496
  break;
320
- case "UA-ULTRA":
321
- return true;
322
497
  default:
323
- // By default, let's assume the wiring is not there.
324
- return false;
498
+ return this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED;
499
+ }
500
+ // Return our REX state. If it's anything other than on, we assume it's open.
501
+ return (this.uda.configs?.find(relay => relay.key === relayType)?.value === "on") ? this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED :
502
+ this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED;
503
+ }
504
+ // Utility to check the wiring state of a given terminal input.
505
+ isWired(input) {
506
+ // UA-ULTRA proxies via button mode.
507
+ if ((this.uda.device_type === "UA-ULTRA") && sensorWiring[input].proxyMode) {
508
+ return this.uda.extensions?.[0]?.target_config?.some(e => e.config_key === "rex_button_mode" && e.config_value === sensorWiring[input].proxyMode) ?? false;
509
+ }
510
+ // Find the wiring keys for this model.
511
+ const wires = sensorWiring[input].wiring?.[this.uda.device_type];
512
+ if (!wires) {
513
+ return false;
325
514
  }
326
- // The DPS is considered wired only if all associated wiring is connected.
327
- return wiringType.filter(wire => this.uda.configs?.some(x => x.key === wire && x.value === "on")).length === wiringType.length;
515
+ // All wires must be on for us to return true.
516
+ return wires.every(wire => this.uda.configs?.some(e => (e.key === wire) && (e.value === "on")));
517
+ }
518
+ // Utility to retrieve a contact sensor state.
519
+ getContactSensorState(name) {
520
+ return this.accessory.getServiceById(this.hap.Service.ContactSensor, name)?.getCharacteristic(this.hap.Characteristic.ContactSensorState).value ??
521
+ this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED;
522
+ }
523
+ // Utility to set a contact sensor state.
524
+ setContactSensorState(name, value) {
525
+ this.accessory.getServiceById(this.hap.Service.ContactSensor, name)?.updateCharacteristic(this.hap.Characteristic.ContactSensorState, value);
328
526
  }
329
527
  // Utility to validate hub capabilities.
330
528
  hasCapability(capability) {
331
- return Array.isArray(capability) ? capability.some(c => this.uda?.capabilities?.includes(c)) : this.uda?.capabilities?.includes(capability);
529
+ return Array.isArray(capability) ? capability.some(c => this.uda.capabilities.includes(c)) : this.uda.capabilities.includes(capability);
332
530
  }
333
531
  // Handle hub-related events.
334
532
  eventHandler(packet) {
533
+ const terminalInputs = [
534
+ { input: "Dps", label: "Door position sensor", topic: "dps" },
535
+ { input: "Rel", label: "Remote release", topic: "rel" },
536
+ { input: "Ren", label: "Request to enter sensor", topic: "ren" },
537
+ { input: "Rex", label: "Request to exit sensor", topic: "rex" }
538
+ ];
335
539
  switch (packet.event) {
336
540
  case "access.data.device.remote_unlock":
337
541
  // Process an Access unlock event.
@@ -351,15 +555,49 @@ export class AccessHub extends AccessDevice {
351
555
  this.log.info(this.hkLockState === this.hap.Characteristic.LockCurrentState.SECURED ? "Locked." : "Unlocked.");
352
556
  }
353
557
  }
354
- // Process a DPS update event if our state has changed.
355
- if (this.hints.hasDps && (this.hubDpsState !== this.hkDpsState)) {
356
- this.hkDpsState = this.hubDpsState;
357
- // Publish to MQTT, if configured to do so.
358
- if (this.isDpsWired) {
359
- this.controller.mqtt?.publish(this.id, "dps", (this.hkDpsState === this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED) ? "false" : "true");
360
- if (this.hints.logDps) {
361
- this.log.info("Door position sensor " + ((this.hkDpsState === this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED) ? "closed." : "open."));
558
+ // Process any terminal input update events if our state has changed.
559
+ for (const { input, topic, label } of terminalInputs) {
560
+ const hasKey = ("hasWiring" + input);
561
+ const hkKey = ("hk" + input + "State");
562
+ const hubKey = ("hub" + input + "State");
563
+ const logKey = ("log" + input);
564
+ const wiredKey = ("is" + input + "Wired");
565
+ if (this.hints[hasKey] && this[hubKey] !== this[hkKey]) {
566
+ this[hkKey] = this[hubKey];
567
+ if (this[wiredKey]) {
568
+ const contactDetected = this[hkKey] === this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED;
569
+ this.controller.mqtt?.publish(this.id, topic, contactDetected ? "false" : "true");
570
+ if (this.hints[logKey]) {
571
+ this.log.info(label + " " + (contactDetected ? "closed" : "open") + ".");
572
+ }
573
+ }
574
+ }
575
+ }
576
+ // Process any changes to terminal input configuration.
577
+ if (packet.data.extensions?.[0]?.target_config && (this.uda.display_model === "UA Ultra")) {
578
+ // Ensure we sync our state with HomeKit.
579
+ this.checkUltraInputs();
580
+ this.configureTerminalInputs();
581
+ }
582
+ // Process any changes to our online status.
583
+ if (packet.data.is_online !== undefined) {
584
+ for (const sensor of Object.keys(AccessReservedNames).filter(key => key.startsWith("CONTACT_"))) {
585
+ this.accessory.getServiceById(this.hap.Service.ContactSensor, AccessReservedNames[sensor])?.
586
+ updateCharacteristic(this.hap.Characteristic.StatusActive, !!packet.data.is_online);
587
+ }
588
+ }
589
+ break;
590
+ case "access.data.v2.device.update":
591
+ if (packet.data.access_method) {
592
+ const accessMethodData = packet.data.access_method;
593
+ // Process access method updates.
594
+ for (const [key, value] of Object.entries(accessMethodData)) {
595
+ const accessMethod = accessMethods.find(entry => entry.key === key);
596
+ if (!accessMethod) {
597
+ continue;
362
598
  }
599
+ // Update any access method switches we have enabled with the current value.
600
+ this.accessory.getServiceById(this.hap.Service.Switch, accessMethod.subtype)?.updateCharacteristic(this.hap.Characteristic.On, value === "yes");
363
601
  }
364
602
  }
365
603
  break;
@@ -398,5 +636,34 @@ export class AccessHub extends AccessDevice {
398
636
  break;
399
637
  }
400
638
  }
639
+ // We dynamically define our getters and setters for terminal inputs so we can streamline redundancies. Yes, this is fancy...but it's meant to future-proof a bit
640
+ // against whatever Ubiquiti may do in the future given the inconsistencies in their API implementation for Access across devices of even similar types.
641
+ static {
642
+ // We define the specific sensor input properties we need.
643
+ for (const input of sensorInputs) {
644
+ let propName = "hk" + input + "State";
645
+ const enumKey = "CONTACT_" + input.toUpperCase();
646
+ Object.defineProperty(AccessHub.prototype, propName, {
647
+ configurable: true,
648
+ enumerable: true,
649
+ get() {
650
+ // Delegate to our individual helper functions.
651
+ return this.getContactSensorState(AccessReservedNames[enumKey]);
652
+ },
653
+ set(value) {
654
+ this.setContactSensorState(AccessReservedNames[enumKey], value);
655
+ }
656
+ });
657
+ // Now define our wiring getters.
658
+ propName = "is" + input + "Wired";
659
+ Object.defineProperty(AccessHub.prototype, propName, {
660
+ configurable: true,
661
+ enumerable: true,
662
+ get() {
663
+ return this.isWired(input);
664
+ }
665
+ });
666
+ }
667
+ }
401
668
  }
402
669
  //# sourceMappingURL=access-hub.js.map