homebridge-unifi-access 1.10.1 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +1 -1
- package/README.md +12 -5
- package/dist/access-controller.js +1 -1
- package/dist/access-device.d.ts +2 -0
- package/dist/access-device.js +1 -1
- package/dist/access-device.js.map +1 -1
- package/dist/access-events.js.map +1 -1
- package/dist/access-hub.d.ts +37 -1
- package/dist/access-hub.js +828 -121
- package/dist/access-hub.js.map +1 -1
- package/dist/access-options.js +11 -6
- package/dist/access-options.js.map +1 -1
- package/dist/access-types.d.ts +3 -0
- package/dist/access-types.js +5 -1
- package/dist/access-types.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/settings.js +1 -1
- package/homebridge-ui/public/lib/featureoptions.js +1 -1
- package/homebridge-ui/public/lib/webUi-featureoptions.mjs +1 -1
- package/homebridge-ui/public/lib/webUi.mjs +1 -1
- package/package.json +9 -6
package/dist/access-hub.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Copyright(C) 2019-
|
|
1
|
+
/* Copyright(C) 2019-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
2
|
*
|
|
3
3
|
* access-hub.ts: Unified hub and reader device class for UniFi Access.
|
|
4
4
|
*/
|
|
@@ -46,17 +46,34 @@ const sensorWiring = {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
|
+
// Constants for timing.
|
|
50
|
+
const AUTO_LOCK_DELAY_MS = 5000;
|
|
51
|
+
const GATE_TRANSITION_COOLDOWN_MS = 5000;
|
|
49
52
|
export class AccessHub extends AccessDevice {
|
|
53
|
+
_hkDpsState;
|
|
50
54
|
_hkLockState;
|
|
55
|
+
_hkSideDoorDpsState;
|
|
56
|
+
_hkSideDoorLockState;
|
|
51
57
|
doorbellRingRequestId;
|
|
58
|
+
gateTransitionUntil;
|
|
52
59
|
lockDelayInterval;
|
|
60
|
+
mainDoorLocationId;
|
|
61
|
+
sideDoorLocationId;
|
|
62
|
+
sideDoorGateTransitionUntil;
|
|
53
63
|
uda;
|
|
54
64
|
// Create an instance.
|
|
55
65
|
constructor(controller, device, accessory) {
|
|
56
66
|
super(controller, accessory);
|
|
57
67
|
this.uda = device;
|
|
68
|
+
this._hkDpsState = this.hubDpsState;
|
|
58
69
|
this._hkLockState = this.hubLockState;
|
|
70
|
+
this._hkSideDoorDpsState = this.hubSideDoorDpsState;
|
|
71
|
+
this._hkSideDoorLockState = this.hubSideDoorLockState;
|
|
72
|
+
this.gateTransitionUntil = 0;
|
|
59
73
|
this.lockDelayInterval = this.getFeatureNumber("Hub.LockDelayInterval") ?? undefined;
|
|
74
|
+
this.mainDoorLocationId = undefined;
|
|
75
|
+
this.sideDoorLocationId = undefined;
|
|
76
|
+
this.sideDoorGateTransitionUntil = 0;
|
|
60
77
|
this.doorbellRingRequestId = null;
|
|
61
78
|
// If we attempt to set the delay interval to something invalid, then assume we are using the default unlock behavior.
|
|
62
79
|
if ((this.lockDelayInterval !== undefined) && (this.lockDelayInterval < 0)) {
|
|
@@ -69,10 +86,12 @@ export class AccessHub extends AccessDevice {
|
|
|
69
86
|
configureHints() {
|
|
70
87
|
// Configure our parent's hints.
|
|
71
88
|
super.configureHints();
|
|
72
|
-
this.hints.
|
|
89
|
+
this.hints.hasSideDoor = (this.uda.device_type === "UGT") && this.hasFeature("Hub.SideDoor");
|
|
90
|
+
this.hints.hasWiringDps = ["UA Ultra", "UA Hub", "UA Hub Door Mini", "UA Gate"].includes(this.uda.display_model ?? "") && this.hasFeature("Hub.DPS");
|
|
73
91
|
this.hints.hasWiringRel = ["UA Hub"].includes(this.uda.display_model ?? "") && this.hasFeature("Hub.REL");
|
|
74
92
|
this.hints.hasWiringRen = ["UA Hub"].includes(this.uda.display_model ?? "") && this.hasFeature("Hub.REN");
|
|
75
93
|
this.hints.hasWiringRex = ["UA Ultra", "UA Hub", "UA Hub Door Mini"].includes(this.uda.display_model ?? "") && this.hasFeature("Hub.REX");
|
|
94
|
+
this.hints.hasWiringSideDoorDps = this.hints.hasSideDoor && this.hasFeature("Hub.SideDoor.DPS");
|
|
76
95
|
this.hints.logDoorbell = this.hasFeature("Log.Doorbell");
|
|
77
96
|
this.hints.logDps = this.hasFeature("Log.DPS");
|
|
78
97
|
this.hints.logLock = this.hasFeature("Log.Lock");
|
|
@@ -86,9 +105,115 @@ export class AccessHub extends AccessDevice {
|
|
|
86
105
|
}
|
|
87
106
|
return true;
|
|
88
107
|
}
|
|
108
|
+
// Return the door service type from configuration. UA Gate devices default to GarageDoorOpener and can be overridden to Lock. Other hubs default to Lock and can be
|
|
109
|
+
// overridden to GarageDoorOpener.
|
|
110
|
+
get doorServiceType() {
|
|
111
|
+
if (this.uda.device_type === "UGT") {
|
|
112
|
+
return this.hasFeature("Hub.Door.UseLock") ? "Lock" : "GarageDoorOpener";
|
|
113
|
+
}
|
|
114
|
+
return this.hasFeature("Hub.Door.UseGarageOpener") ? "GarageDoorOpener" : "Lock";
|
|
115
|
+
}
|
|
116
|
+
// Convert lock string value to HomeKit LockCurrentState.
|
|
117
|
+
toLockState(lockValue) {
|
|
118
|
+
return ["unlock", "unlocked"].includes(lockValue) ? this.hap.Characteristic.LockCurrentState.UNSECURED : this.hap.Characteristic.LockCurrentState.SECURED;
|
|
119
|
+
}
|
|
120
|
+
// Convert DPS string value to HomeKit ContactSensorState.
|
|
121
|
+
toDpsState(dpsValue) {
|
|
122
|
+
return dpsValue === "open" ? this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED;
|
|
123
|
+
}
|
|
124
|
+
// Check if a lock state represents "locked".
|
|
125
|
+
isLocked(state) {
|
|
126
|
+
return state === this.hap.Characteristic.LockCurrentState.SECURED;
|
|
127
|
+
}
|
|
128
|
+
// Check if a DPS state represents "closed" (contact detected).
|
|
129
|
+
isClosed(state) {
|
|
130
|
+
return state === this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED;
|
|
131
|
+
}
|
|
132
|
+
// Discover main and side door location IDs for UA Gate hubs. This allows us to receive remote_unlock events for each door.
|
|
133
|
+
discoverDoorIds() {
|
|
134
|
+
const doors = this.controller.udaApi.doors ?? [];
|
|
135
|
+
if (doors.length === 0) {
|
|
136
|
+
this.log.warn("No doors found in Access API. Door event handling may not work correctly.");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// Get the primary door ID from device config (may be undefined).
|
|
140
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
141
|
+
const primaryDoorId = this.uda.door?.unique_id;
|
|
142
|
+
// Strategy 1: Use the device's bound door as main door.
|
|
143
|
+
if (primaryDoorId) {
|
|
144
|
+
this.mainDoorLocationId = primaryDoorId;
|
|
145
|
+
}
|
|
146
|
+
else if (doors.length >= 1) {
|
|
147
|
+
// Strategy 2: Look for a door named like "main", "gate", "portail" (but not side/pedestrian).
|
|
148
|
+
const mainDoor = doors.find(door => /portail|main|gate|principal|entry|front/i.test(door.name) && !/portillon|side|pedestrian|pieton|wicket|back/i.test(door.name));
|
|
149
|
+
// Strategy 3: Use the first door as main door.
|
|
150
|
+
this.mainDoorLocationId = mainDoor?.unique_id ?? doors[0].unique_id;
|
|
151
|
+
}
|
|
152
|
+
// Find the side door (if enabled).
|
|
153
|
+
if (this.hints.hasSideDoor) {
|
|
154
|
+
// Strategy 1: Check extensions for oper2 port setting.
|
|
155
|
+
const sideDoorFromExt = this.uda.extensions?.find(ext => (ext.extension_name === "port_setting") && (ext.target_name === "oper2"))?.target_value;
|
|
156
|
+
if (sideDoorFromExt) {
|
|
157
|
+
this.sideDoorLocationId = sideDoorFromExt;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// Strategy 2: Look for a door named like "side", "portillon", "pedestrian".
|
|
161
|
+
const sideDoor = doors.find(door => (door.unique_id !== this.mainDoorLocationId) && /portillon|side|pedestrian|pieton|wicket|back|secondary/i.test(door.name));
|
|
162
|
+
if (sideDoor) {
|
|
163
|
+
this.sideDoorLocationId = sideDoor.unique_id;
|
|
164
|
+
}
|
|
165
|
+
else if (doors.length === 2) {
|
|
166
|
+
// Strategy 3: If we have exactly 2 doors, the other one is the side door.
|
|
167
|
+
const otherDoor = doors.find(door => door.unique_id !== this.mainDoorLocationId);
|
|
168
|
+
this.sideDoorLocationId = otherDoor?.unique_id;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Initialize door states from the already-loaded doors data.
|
|
173
|
+
this.initializeDoorsFromBootstrap(doors);
|
|
174
|
+
}
|
|
175
|
+
// Initialize door states from the doors data loaded during API bootstrap. This avoids making additional API calls which may fail.
|
|
176
|
+
initializeDoorsFromBootstrap(doors) {
|
|
177
|
+
// Find and initialize main door state.
|
|
178
|
+
if (this.mainDoorLocationId) {
|
|
179
|
+
const mainDoor = doors.find(d => d.unique_id === this.mainDoorLocationId);
|
|
180
|
+
if (mainDoor) {
|
|
181
|
+
const dpsStatus = mainDoor.door_position_status ?? "close";
|
|
182
|
+
const lockStatus = mainDoor.door_lock_relay_status ?? "lock";
|
|
183
|
+
// Set DPS state.
|
|
184
|
+
const newDpsState = dpsStatus === "open" ? this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED :
|
|
185
|
+
this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED;
|
|
186
|
+
this.hkDpsState = newDpsState;
|
|
187
|
+
// Set lock state.
|
|
188
|
+
const newLockState = lockStatus === "unlock" ? this.hap.Characteristic.LockCurrentState.UNSECURED : this.hap.Characteristic.LockCurrentState.SECURED;
|
|
189
|
+
this._hkLockState = newLockState;
|
|
190
|
+
// Update the door service.
|
|
191
|
+
if (this.doorServiceType === "GarageDoorOpener") {
|
|
192
|
+
this.updateDoorServiceState(false);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Find and initialize side door state.
|
|
197
|
+
if (this.sideDoorLocationId && this.hints.hasSideDoor) {
|
|
198
|
+
const sideDoor = doors.find(d => d.unique_id === this.sideDoorLocationId);
|
|
199
|
+
if (sideDoor) {
|
|
200
|
+
const dpsStatus = sideDoor.door_position_status ?? "close";
|
|
201
|
+
const lockStatus = sideDoor.door_lock_relay_status ?? "lock";
|
|
202
|
+
// Set DPS state.
|
|
203
|
+
const newDpsState = dpsStatus === "open" ? this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED :
|
|
204
|
+
this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED;
|
|
205
|
+
this._hkSideDoorDpsState = newDpsState;
|
|
206
|
+
// Set lock state.
|
|
207
|
+
const newLockState = lockStatus === "unlock" ? this.hap.Characteristic.LockCurrentState.UNSECURED : this.hap.Characteristic.LockCurrentState.SECURED;
|
|
208
|
+
this._hkSideDoorLockState = newLockState;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
89
212
|
// Initialize and configure the light accessory for HomeKit.
|
|
90
213
|
configureDevice() {
|
|
91
214
|
this._hkLockState = this.hubLockState;
|
|
215
|
+
this._hkSideDoorDpsState = this.hubSideDoorDpsState;
|
|
216
|
+
this._hkSideDoorLockState = this.hubSideDoorLockState;
|
|
92
217
|
// Clean out the context object in case it's been polluted somehow.
|
|
93
218
|
this.accessory.context = {};
|
|
94
219
|
this.accessory.context.mac = this.uda.mac;
|
|
@@ -99,24 +224,49 @@ export class AccessHub extends AccessDevice {
|
|
|
99
224
|
else {
|
|
100
225
|
this.log.info("The door lock relay will remain unlocked %s after unlocking in HomeKit.", this.lockDelayInterval === 0 ? "indefinitely" : "for " + this.lockDelayInterval.toString() + " minutes");
|
|
101
226
|
}
|
|
227
|
+
if (this.hints.hasSideDoor) {
|
|
228
|
+
if (this.lockDelayInterval === undefined) {
|
|
229
|
+
this.log.info("The side door lock relay will lock five seconds after unlocking in HomeKit.");
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
this.log.info("The side door lock relay will remain unlocked %s after unlocking in HomeKit.", this.lockDelayInterval === 0 ? "indefinitely" : "for " + this.lockDelayInterval.toString() + " minutes");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
102
235
|
// Configure accessory information.
|
|
103
236
|
this.configureInfo();
|
|
104
237
|
// Configure access method switches, if we're a reader device.
|
|
105
238
|
this.configureAccessMethodSwitches();
|
|
239
|
+
// Configure the sensors connected to terminal inputs. This must be done before configuring the lock so that the DPS contact sensor exists when configuring a
|
|
240
|
+
// GarageDoorOpener service, which derives its state from the DPS sensor.
|
|
241
|
+
this.configureTerminalInputs();
|
|
242
|
+
this.configureSideDoorTerminalInputs();
|
|
106
243
|
// Configure the lock, if we're a hub device.
|
|
107
244
|
this.configureLock();
|
|
108
245
|
this.configureLockTrigger();
|
|
246
|
+
// Configure the side door lock, if we're a UA Gate device.
|
|
247
|
+
this.configureSideDoorLock();
|
|
248
|
+
this.configureSideDoorLockTrigger();
|
|
109
249
|
// Configure the doorbell, if we have one.
|
|
110
250
|
this.configureDoorbell();
|
|
111
251
|
this.configureDoorbellTrigger();
|
|
112
|
-
// Configure the sensors connected to terminal inputs.
|
|
113
|
-
this.configureTerminalInputs();
|
|
114
252
|
// Configure MQTT services.
|
|
115
253
|
this.configureMqtt();
|
|
116
254
|
// Listen for events.
|
|
117
255
|
this.controller.events.on(this.uda.unique_id, this.listeners[this.uda.unique_id] = this.eventHandler.bind(this));
|
|
118
256
|
this.controller.events.on("access.remote_view", this.listeners["access.remote_view"] = this.eventHandler.bind(this));
|
|
119
257
|
this.controller.events.on("access.remote_view.change", this.listeners["access.remote_view.change"] = this.eventHandler.bind(this));
|
|
258
|
+
// For UA Gate hubs, we discover door IDs and subscribe to their events. This is needed because remote_unlock events use the door's location_id as the
|
|
259
|
+
// event_object_id, not the hub's device_id.
|
|
260
|
+
if (this.uda.device_type === "UGT") {
|
|
261
|
+
this.discoverDoorIds();
|
|
262
|
+
// Subscribe to events for both doors.
|
|
263
|
+
if (this.mainDoorLocationId) {
|
|
264
|
+
this.controller.events.on(this.mainDoorLocationId, this.listeners[this.mainDoorLocationId] = this.eventHandler.bind(this));
|
|
265
|
+
}
|
|
266
|
+
if (this.sideDoorLocationId) {
|
|
267
|
+
this.controller.events.on(this.sideDoorLocationId, this.listeners[this.sideDoorLocationId] = this.eventHandler.bind(this));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
120
270
|
return true;
|
|
121
271
|
}
|
|
122
272
|
// Configure the access method switches for HomeKit.
|
|
@@ -212,28 +362,71 @@ export class AccessHub extends AccessDevice {
|
|
|
212
362
|
}
|
|
213
363
|
return true;
|
|
214
364
|
}
|
|
215
|
-
// Configure
|
|
365
|
+
// Configure contact sensors for the side door terminal inputs on UA Gate hubs. The side door has its own dedicated DPS input separate from the main gate's DPS.
|
|
366
|
+
configureSideDoorTerminalInputs() {
|
|
367
|
+
// We only configure side door terminal inputs for UA Gate hubs that have the side door enabled.
|
|
368
|
+
if (!this.hints.hasSideDoor) {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
// Validate whether we should have this service enabled. We check the hasWiringSideDoorDps hint which already incorporates the feature option check.
|
|
372
|
+
if (!validService(this.accessory, this.hap.Service.ContactSensor, (hasService) => {
|
|
373
|
+
if (!this.hints.hasWiringSideDoorDps && hasService) {
|
|
374
|
+
this.log.info("Disabling the side door position sensor.");
|
|
375
|
+
}
|
|
376
|
+
return this.hints.hasWiringSideDoorDps;
|
|
377
|
+
}, AccessReservedNames.CONTACT_DPS_SIDE)) {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
// Acquire the service.
|
|
381
|
+
const service = acquireService(this.accessory, this.hap.Service.ContactSensor, this.accessoryName + " Side Door Position Sensor", AccessReservedNames.CONTACT_DPS_SIDE, () => this.log.info("Enabling the side door position sensor."));
|
|
382
|
+
if (!service) {
|
|
383
|
+
this.log.error("Unable to add the side door position sensor.");
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
// Initialize the sensor state from the current side door DPS state.
|
|
387
|
+
service.updateCharacteristic(this.hap.Characteristic.ContactSensorState, this._hkSideDoorDpsState);
|
|
388
|
+
service.updateCharacteristic(this.hap.Characteristic.StatusActive, !!this.uda.is_online);
|
|
389
|
+
// If the hub has tamper indicator capabilities, reflect that in HomeKit.
|
|
390
|
+
if (this.hasCapability("tamper_proofing")) {
|
|
391
|
+
const tamperedEntry = this.uda.configs?.find(entry => entry.key === "tamper_event");
|
|
392
|
+
if (tamperedEntry) {
|
|
393
|
+
service.updateCharacteristic(this.hap.Characteristic.StatusTampered, (tamperedEntry.value === "true") ? this.hap.Characteristic.StatusTampered.TAMPERED :
|
|
394
|
+
this.hap.Characteristic.StatusTampered.NOT_TAMPERED);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
// Configure the door for HomeKit. Supports Lock and GarageDoorOpener service types.
|
|
216
400
|
configureLock() {
|
|
401
|
+
// First, remove any previous service types that are no longer selected.
|
|
402
|
+
const serviceTypes = [this.hap.Service.LockMechanism, this.hap.Service.GarageDoorOpener];
|
|
403
|
+
const selectedService = this.doorServiceType === "GarageDoorOpener" ? this.hap.Service.GarageDoorOpener : this.hap.Service.LockMechanism;
|
|
404
|
+
for (const serviceType of serviceTypes) {
|
|
405
|
+
if (serviceType !== selectedService) {
|
|
406
|
+
const oldService = this.accessory.getService(serviceType);
|
|
407
|
+
if (oldService) {
|
|
408
|
+
this.accessory.removeService(oldService);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
217
412
|
// Validate whether we should have this service enabled.
|
|
218
|
-
if (!validService(this.accessory,
|
|
413
|
+
if (!validService(this.accessory, selectedService, this.hasCapability("is_hub"))) {
|
|
219
414
|
return false;
|
|
220
415
|
}
|
|
221
416
|
// Acquire the service.
|
|
222
|
-
const service = acquireService(this.accessory, this.
|
|
417
|
+
const service = acquireService(this.accessory, selectedService, this.accessoryName, undefined, () => this.log.info("Configuring door as %s service.", this.doorServiceType));
|
|
223
418
|
if (!service) {
|
|
224
|
-
this.log.error("Unable to add the
|
|
419
|
+
this.log.error("Unable to add the door.");
|
|
225
420
|
return false;
|
|
226
421
|
}
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
});
|
|
236
|
-
// Initialize the lock.
|
|
422
|
+
// Configure based on service type.
|
|
423
|
+
if (this.doorServiceType === "GarageDoorOpener") {
|
|
424
|
+
this.configureGarageDoorService(service, false);
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
this.configureLockService(service, false);
|
|
428
|
+
}
|
|
429
|
+
// Initialize the state.
|
|
237
430
|
this._hkLockState = -1;
|
|
238
431
|
service.displayName = this.accessoryName;
|
|
239
432
|
service.updateCharacteristic(this.hap.Characteristic.Name, this.accessoryName);
|
|
@@ -241,6 +434,100 @@ export class AccessHub extends AccessDevice {
|
|
|
241
434
|
service.setPrimaryService(true);
|
|
242
435
|
return true;
|
|
243
436
|
}
|
|
437
|
+
// Configure a LockMechanism service.
|
|
438
|
+
configureLockService(service, isSideDoor) {
|
|
439
|
+
if (!service) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const lockStateGetter = isSideDoor ? () => this.hkSideDoorLockState : () => this.hkLockState;
|
|
443
|
+
const lockCommand = isSideDoor ? async (lock) => this.hubSideDoorLockCommand(lock) :
|
|
444
|
+
async (lock) => this.hubLockCommand(lock);
|
|
445
|
+
service.getCharacteristic(this.hap.Characteristic.LockCurrentState).onGet(lockStateGetter);
|
|
446
|
+
service.getCharacteristic(this.hap.Characteristic.LockTargetState).onGet(lockStateGetter);
|
|
447
|
+
service.getCharacteristic(this.hap.Characteristic.LockTargetState).onSet(async (value) => {
|
|
448
|
+
// Check if this is just syncing state from an event (current state already matches target).
|
|
449
|
+
const currentState = lockStateGetter();
|
|
450
|
+
const targetLocked = value === this.hap.Characteristic.LockTargetState.SECURED;
|
|
451
|
+
const currentlyLocked = currentState === this.hap.Characteristic.LockCurrentState.SECURED;
|
|
452
|
+
// If state already matches, this is just a sync from an event - don't send command.
|
|
453
|
+
if (targetLocked === currentlyLocked) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (!(await lockCommand(targetLocked))) {
|
|
457
|
+
setTimeout(() => service.updateCharacteristic(this.hap.Characteristic.LockTargetState, currentlyLocked ? this.hap.Characteristic.LockTargetState.SECURED : this.hap.Characteristic.LockTargetState.UNSECURED), 50);
|
|
458
|
+
}
|
|
459
|
+
service.updateCharacteristic(this.hap.Characteristic.LockCurrentState, lockStateGetter());
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
// Configure a GarageDoorOpener service.
|
|
463
|
+
configureGarageDoorService(service, isSideDoor) {
|
|
464
|
+
if (!service) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
// For UA Gate, we use unlock/trigger command for both open and close operations. The gate motor will move in the appropriate direction based on its current state.
|
|
468
|
+
// For non-UA Gate hubs, we use lock/unlock commands directly (locked = closed, unlocked = open).
|
|
469
|
+
const isUaGate = this.uda.device_type === "UGT";
|
|
470
|
+
// Determine the current door state. For UA Gate, we use DPS (Door Position Sensor) state since it's a motorized gate with physical positions. For non-UA Gate hubs,
|
|
471
|
+
// we derive state from the lock relay (locked = closed, unlocked = open) since GarageDoorOpener is just a visual convenience for the same underlying lock behavior.
|
|
472
|
+
const getDoorState = () => {
|
|
473
|
+
if (isUaGate) {
|
|
474
|
+
const dpsState = isSideDoor ? this._hkSideDoorDpsState : this.hkDpsState;
|
|
475
|
+
return dpsState === this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED ? this.hap.Characteristic.CurrentDoorState.CLOSED :
|
|
476
|
+
this.hap.Characteristic.CurrentDoorState.OPEN;
|
|
477
|
+
}
|
|
478
|
+
// Non-UA Gate hubs: derive from lock relay state.
|
|
479
|
+
const lockState = isSideDoor ? this.hkSideDoorLockState : this.hkLockState;
|
|
480
|
+
return this.isLocked(lockState) ? this.hap.Characteristic.CurrentDoorState.CLOSED : this.hap.Characteristic.CurrentDoorState.OPEN;
|
|
481
|
+
};
|
|
482
|
+
service.getCharacteristic(this.hap.Characteristic.CurrentDoorState).onGet(getDoorState);
|
|
483
|
+
service.getCharacteristic(this.hap.Characteristic.TargetDoorState).onSet(async (value) => {
|
|
484
|
+
const shouldClose = value === this.hap.Characteristic.TargetDoorState.CLOSED;
|
|
485
|
+
// UA Gate uses a single trigger command that toggles the motorized gate. Non-UA Gate hubs use explicit lock/unlock commands.
|
|
486
|
+
if (isUaGate) {
|
|
487
|
+
// Set a transition cooldown to prevent WebSocket events from immediately reverting the door state. This gives the gate time to physically move before we accept
|
|
488
|
+
// DPS updates.
|
|
489
|
+
if (isSideDoor) {
|
|
490
|
+
this.sideDoorGateTransitionUntil = Date.now() + GATE_TRANSITION_COOLDOWN_MS;
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
this.gateTransitionUntil = Date.now() + GATE_TRANSITION_COOLDOWN_MS;
|
|
494
|
+
}
|
|
495
|
+
// Immediately show transitional state (Opening/Closing) while the door moves.
|
|
496
|
+
service.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, shouldClose ? this.hap.Characteristic.CurrentDoorState.CLOSING :
|
|
497
|
+
this.hap.Characteristic.CurrentDoorState.OPENING);
|
|
498
|
+
// Trigger the gate - for motorized gates, the same trigger command handles both open and close.
|
|
499
|
+
const triggerGate = isSideDoor ? async () => this.hubSideDoorLockCommand(false) : async () => this.hubLockCommand(false);
|
|
500
|
+
if (!(await triggerGate())) {
|
|
501
|
+
// Clear the transition cooldown on failure.
|
|
502
|
+
if (isSideDoor) {
|
|
503
|
+
this.sideDoorGateTransitionUntil = 0;
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
this.gateTransitionUntil = 0;
|
|
507
|
+
}
|
|
508
|
+
// Revert target state on failure.
|
|
509
|
+
setTimeout(() => {
|
|
510
|
+
service.updateCharacteristic(this.hap.Characteristic.TargetDoorState, shouldClose ? this.hap.Characteristic.TargetDoorState.OPEN : this.hap.Characteristic.TargetDoorState.CLOSED);
|
|
511
|
+
service.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, getDoorState());
|
|
512
|
+
}, 50);
|
|
513
|
+
}
|
|
514
|
+
// The DPS sensor event will update the CurrentDoorState when the gate finishes moving.
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
// Non-UA Gate hubs: use lock/unlock commands directly (close = lock, open = unlock). The lock state change will drive the door state update via hkLockState setter.
|
|
518
|
+
const lockCommand = isSideDoor ? async (lock) => this.hubSideDoorLockCommand(lock) :
|
|
519
|
+
async (lock) => this.hubLockCommand(lock);
|
|
520
|
+
if (!(await lockCommand(shouldClose))) {
|
|
521
|
+
// Revert target state on failure.
|
|
522
|
+
setTimeout(() => {
|
|
523
|
+
service.updateCharacteristic(this.hap.Characteristic.TargetDoorState, shouldClose ? this.hap.Characteristic.TargetDoorState.OPEN : this.hap.Characteristic.TargetDoorState.CLOSED);
|
|
524
|
+
service.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, getDoorState());
|
|
525
|
+
}, 50);
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
// ObstructionDetected is required - we always report no obstruction.
|
|
529
|
+
service.getCharacteristic(this.hap.Characteristic.ObstructionDetected).onGet(() => false);
|
|
530
|
+
}
|
|
244
531
|
// Configure a switch to manually trigger a doorbell ring event for HomeKit.
|
|
245
532
|
configureDoorbellTrigger() {
|
|
246
533
|
// Validate whether we should have this service enabled.
|
|
@@ -291,6 +578,54 @@ export class AccessHub extends AccessDevice {
|
|
|
291
578
|
service.updateCharacteristic(this.hap.Characteristic.On, false);
|
|
292
579
|
return true;
|
|
293
580
|
}
|
|
581
|
+
// Configure the side door for HomeKit (UA Gate only) - always uses Lock service.
|
|
582
|
+
configureSideDoorLock() {
|
|
583
|
+
// Validate whether we should have this service enabled.
|
|
584
|
+
if (!validService(this.accessory, this.hap.Service.LockMechanism, this.hints.hasSideDoor, AccessReservedNames.LOCK_DOOR_SIDE)) {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
// Acquire the service.
|
|
588
|
+
const service = acquireService(this.accessory, this.hap.Service.LockMechanism, this.accessoryName + " Side Door", AccessReservedNames.LOCK_DOOR_SIDE, () => this.log.info("Configuring side door lock."));
|
|
589
|
+
if (!service) {
|
|
590
|
+
this.log.error("Unable to add the side door.");
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
// Configure the lock service.
|
|
594
|
+
this.configureLockService(service, true);
|
|
595
|
+
// Initialize the lock.
|
|
596
|
+
this._hkSideDoorLockState = -1;
|
|
597
|
+
service.displayName = this.accessoryName + " Side Door";
|
|
598
|
+
service.updateCharacteristic(this.hap.Characteristic.Name, this.accessoryName + " Side Door");
|
|
599
|
+
this.hkSideDoorLockState = this.hubSideDoorLockState;
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
// Configure a switch to automate side door lock and unlock events in HomeKit beyond what HomeKit might allow for a lock service that gets treated as a secure service.
|
|
603
|
+
configureSideDoorLockTrigger() {
|
|
604
|
+
// Validate whether we should have this service enabled.
|
|
605
|
+
if (!validService(this.accessory, this.hap.Service.Switch, this.hints.hasSideDoor && this.hasFeature("Hub.SideDoor.Lock.Trigger"), AccessReservedNames.SWITCH_LOCK_DOOR_SIDE_TRIGGER)) {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
// Acquire the service.
|
|
609
|
+
const service = acquireService(this.accessory, this.hap.Service.Switch, this.accessoryName + " Side Door Lock Trigger", AccessReservedNames.SWITCH_LOCK_DOOR_SIDE_TRIGGER, () => this.log.info("Enabling the side door lock automation trigger."));
|
|
610
|
+
if (!service) {
|
|
611
|
+
this.log.error("Unable to add the side door lock automation trigger.");
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
// Trigger the lock state.
|
|
615
|
+
service.getCharacteristic(this.hap.Characteristic.On).onGet(() => this.hkSideDoorLockState !== this.hap.Characteristic.LockCurrentState.SECURED);
|
|
616
|
+
// The state isn't really user-triggerable. We have no way, currently, to trigger a lock or unlock event on the hub.
|
|
617
|
+
service.getCharacteristic(this.hap.Characteristic.On).onSet(async (value) => {
|
|
618
|
+
// If we are on, we are in an unlocked state. If we are off, we are in a locked state.
|
|
619
|
+
if (!(await this.hubSideDoorLockCommand(!value))) {
|
|
620
|
+
// Revert our state.
|
|
621
|
+
setTimeout(() => service.updateCharacteristic(this.hap.Characteristic.On, !value), 50);
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
// Initialize the switch.
|
|
625
|
+
service.updateCharacteristic(this.hap.Characteristic.ConfiguredName, this.accessoryName + " Side Door Lock Trigger");
|
|
626
|
+
service.updateCharacteristic(this.hap.Characteristic.On, false);
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
294
629
|
// Configure MQTT capabilities of this light.
|
|
295
630
|
configureMqtt() {
|
|
296
631
|
const lockService = this.accessory.getService(this.hap.Service.LockMechanism);
|
|
@@ -340,6 +675,46 @@ export class AccessHub extends AccessDevice {
|
|
|
340
675
|
break;
|
|
341
676
|
}
|
|
342
677
|
});
|
|
678
|
+
// MQTT side door lock status (UA Gate only).
|
|
679
|
+
if (this.hints.hasSideDoor) {
|
|
680
|
+
this.controller.mqtt?.subscribeGet(this.id, "sidedoor/lock", "Side Door Lock", () => {
|
|
681
|
+
switch (this.hkSideDoorLockState) {
|
|
682
|
+
case this.hap.Characteristic.LockCurrentState.SECURED:
|
|
683
|
+
return "true";
|
|
684
|
+
case this.hap.Characteristic.LockCurrentState.UNSECURED:
|
|
685
|
+
return "false";
|
|
686
|
+
default:
|
|
687
|
+
return "unknown";
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
this.controller.mqtt?.subscribeSet(this.id, "sidedoor/lock", "Side Door Lock", (value) => {
|
|
691
|
+
switch (value) {
|
|
692
|
+
case "true":
|
|
693
|
+
void this.hubSideDoorLockCommand(true);
|
|
694
|
+
break;
|
|
695
|
+
case "false":
|
|
696
|
+
void this.hubSideDoorLockCommand(false);
|
|
697
|
+
break;
|
|
698
|
+
default:
|
|
699
|
+
this.log.error("MQTT: Unknown side door lock set message received: %s.", value);
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
// MQTT side door DPS status (UA Gate only).
|
|
704
|
+
this.controller.mqtt?.subscribeGet(this.id, "sidedoor/dps", "Side door position sensor", () => {
|
|
705
|
+
if (!this.isSideDoorDpsWired) {
|
|
706
|
+
return "unknown";
|
|
707
|
+
}
|
|
708
|
+
switch (this._hkSideDoorDpsState) {
|
|
709
|
+
case this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED:
|
|
710
|
+
return "false";
|
|
711
|
+
case this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED:
|
|
712
|
+
return "true";
|
|
713
|
+
default:
|
|
714
|
+
return "unknown";
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
}
|
|
343
718
|
return true;
|
|
344
719
|
}
|
|
345
720
|
// Check and validate Ultra inputs with what the user has configured in HomeKit.
|
|
@@ -348,7 +723,7 @@ export class AccessHub extends AccessDevice {
|
|
|
348
723
|
const hint = ("hasWiring" + input);
|
|
349
724
|
const mode = input.toLowerCase();
|
|
350
725
|
// 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);
|
|
726
|
+
const isEnabled = this.uda.extensions?.[0]?.target_config?.some(entry => (entry.config_key === "rex_button_mode") && (entry.config_value === mode));
|
|
352
727
|
if (this.hints[hint] && !isEnabled) {
|
|
353
728
|
// The hub has disabled this input.
|
|
354
729
|
this.hints[hint] = false;
|
|
@@ -359,26 +734,91 @@ export class AccessHub extends AccessDevice {
|
|
|
359
734
|
}
|
|
360
735
|
}
|
|
361
736
|
}
|
|
362
|
-
//
|
|
363
|
-
async
|
|
737
|
+
// Unified utility function to execute lock and unlock actions on a hub door.
|
|
738
|
+
async hubDoorLockCommand(isLocking, isSideDoor = false) {
|
|
364
739
|
const action = isLocking ? "lock" : "unlock";
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
740
|
+
const doorName = isSideDoor ? "side door" : (this.uda.device_type === "UGT" ? "gate" : "door");
|
|
741
|
+
const doorId = isSideDoor ? this.sideDoorLocationId : this.mainDoorLocationId;
|
|
742
|
+
// Only allow relocking if we are able to do so. UA Gate is exempt since it's a motorized gate that needs to close. For non-UA Gate hubs, the same restriction
|
|
743
|
+
// applies to both Lock and GarageDoorOpener service types since GarageDoorOpener is just a visual convenience for the same underlying lock behavior.
|
|
744
|
+
if ((this.lockDelayInterval === undefined) && isLocking && (this.uda.device_type !== "UGT")) {
|
|
745
|
+
this.log.error("Unable to manually relock the %s when the lock relay is configured to the default settings.", doorName);
|
|
368
746
|
return false;
|
|
369
747
|
}
|
|
370
748
|
// If we're not online, we're done.
|
|
371
749
|
if (!this.isOnline) {
|
|
372
|
-
this.log.error("Unable to %s. Device is offline.", action);
|
|
750
|
+
this.log.error("Unable to %s the %s. Device is offline.", action, doorName);
|
|
373
751
|
return false;
|
|
374
752
|
}
|
|
753
|
+
// For UA Gate hubs, use the location-based unlock API since the device API is not supported.
|
|
754
|
+
if (this.uda.device_type === "UGT") {
|
|
755
|
+
if (!doorId) {
|
|
756
|
+
this.log.error("Unable to %s the %s. Door not found.", action, isSideDoor ? "side door" : "gate");
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
// Execute the action using the location endpoint.
|
|
760
|
+
const endpoint = this.controller.udaApi.getApiEndpoint("location") + "/" + doorId + "/unlock";
|
|
761
|
+
const response = await this.controller.udaApi.retrieve(endpoint, {
|
|
762
|
+
body: JSON.stringify({}),
|
|
763
|
+
method: "PUT"
|
|
764
|
+
});
|
|
765
|
+
if (!this.controller.udaApi.responseOk(response?.statusCode)) {
|
|
766
|
+
this.log.error("Unable to %s the %s.", action, doorName);
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
// When unlocking from HomeKit, the controller doesn't send the events to the events API. Manually update the state and schedule the auto-lock.
|
|
770
|
+
if (!isLocking) {
|
|
771
|
+
if (isSideDoor) {
|
|
772
|
+
this.hkSideDoorLockState = this.hap.Characteristic.LockCurrentState.UNSECURED;
|
|
773
|
+
if (this.hints.logLock) {
|
|
774
|
+
this.log.info("Side door unlocked.");
|
|
775
|
+
}
|
|
776
|
+
setTimeout(() => {
|
|
777
|
+
this.hkSideDoorLockState = this.hap.Characteristic.LockCurrentState.SECURED;
|
|
778
|
+
if (this.hints.logLock) {
|
|
779
|
+
this.log.info("Side door locked.");
|
|
780
|
+
}
|
|
781
|
+
}, AUTO_LOCK_DELAY_MS);
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
this.hkLockState = this.hap.Characteristic.LockCurrentState.UNSECURED;
|
|
785
|
+
setTimeout(() => this.hkLockState = this.hap.Characteristic.LockCurrentState.SECURED, AUTO_LOCK_DELAY_MS);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
// For hub types other than UA Gate, we use the standard device unlock API. GarageDoorOpener uses the same lock delay interval as Lock service since it's just a
|
|
791
|
+
// visual convenience for the same underlying lock behavior.
|
|
792
|
+
const delayInterval = this.lockDelayInterval;
|
|
375
793
|
// Execute the action.
|
|
376
|
-
if (!(await this.controller.udaApi.unlock(this.uda, (
|
|
794
|
+
if (!(await this.controller.udaApi.unlock(this.uda, (delayInterval === undefined) ? undefined : (isLocking ? 0 : Infinity)))) {
|
|
377
795
|
this.log.error("Unable to %s.", action);
|
|
378
796
|
return false;
|
|
379
797
|
}
|
|
380
798
|
return true;
|
|
381
799
|
}
|
|
800
|
+
// Wrapper for door lock command.
|
|
801
|
+
async hubLockCommand(isLocking) {
|
|
802
|
+
return this.hubDoorLockCommand(isLocking);
|
|
803
|
+
}
|
|
804
|
+
// Wrapper for side door lock command (for backwards compatibility).
|
|
805
|
+
async hubSideDoorLockCommand(isLocking) {
|
|
806
|
+
return this.hubDoorLockCommand(isLocking, true);
|
|
807
|
+
}
|
|
808
|
+
// Return the current HomeKit DPS state that we are tracking for this hub. We read from the contact sensor service if it exists, otherwise we fall back to the
|
|
809
|
+
// backing variable. This allows GarageDoorOpener to function correctly even when the DPS contact sensor is disabled.
|
|
810
|
+
get hkDpsState() {
|
|
811
|
+
const service = this.accessory.getServiceById(this.hap.Service.ContactSensor, AccessReservedNames.CONTACT_DPS);
|
|
812
|
+
if (service) {
|
|
813
|
+
return service.getCharacteristic(this.hap.Characteristic.ContactSensorState).value ?? this._hkDpsState;
|
|
814
|
+
}
|
|
815
|
+
return this._hkDpsState;
|
|
816
|
+
}
|
|
817
|
+
// Set the current HomeKit DPS state for this hub. We always update the backing variable and also update the contact sensor service if it exists.
|
|
818
|
+
set hkDpsState(value) {
|
|
819
|
+
this._hkDpsState = value;
|
|
820
|
+
this.setContactSensorState(AccessReservedNames.CONTACT_DPS, value);
|
|
821
|
+
}
|
|
382
822
|
// Return the current HomeKit lock state that we are tracking for this hub.
|
|
383
823
|
get hkLockState() {
|
|
384
824
|
return this._hkLockState;
|
|
@@ -391,16 +831,79 @@ export class AccessHub extends AccessDevice {
|
|
|
391
831
|
}
|
|
392
832
|
// Update the lock state.
|
|
393
833
|
this._hkLockState = value;
|
|
394
|
-
//
|
|
395
|
-
|
|
396
|
-
if (
|
|
834
|
+
// For Lock service type, update the service. For GarageDoorOpener on non-UA Gate hubs, also update the service since door state is derived from lock state. For UA
|
|
835
|
+
// Gate with GarageDoorOpener, DPS events handle updates since it's a motorized gate with physical positions.
|
|
836
|
+
if ((this.doorServiceType === "Lock") || (this.uda.device_type !== "UGT")) {
|
|
837
|
+
this.updateDoorServiceState(false);
|
|
838
|
+
}
|
|
839
|
+
else {
|
|
840
|
+
// UA Gate with GarageDoorOpener: only update the lock trigger switch if enabled.
|
|
841
|
+
this.accessory.getServiceById(this.hap.Service.Switch, AccessReservedNames.SWITCH_LOCK_TRIGGER)?.updateCharacteristic(this.hap.Characteristic.On, value !== this.hap.Characteristic.LockCurrentState.SECURED);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
// Update door service state based on configured service type.
|
|
845
|
+
updateDoorServiceState(isSideDoor) {
|
|
846
|
+
const serviceType = isSideDoor ? "Lock" : this.doorServiceType;
|
|
847
|
+
const lockState = isSideDoor ? this.hkSideDoorLockState : this.hkLockState;
|
|
848
|
+
const triggerSubtype = isSideDoor ? AccessReservedNames.SWITCH_LOCK_DOOR_SIDE_TRIGGER : AccessReservedNames.SWITCH_LOCK_TRIGGER;
|
|
849
|
+
// Check if we're in a transition cooldown period - skip updates to preserve the Opening/Closing state.
|
|
850
|
+
const transitionUntil = isSideDoor ? this.sideDoorGateTransitionUntil : this.gateTransitionUntil;
|
|
851
|
+
if (serviceType === "GarageDoorOpener") {
|
|
852
|
+
// GarageDoorOpener is only used for the primary door, which has no subtype.
|
|
853
|
+
const service = this.accessory.getService(this.hap.Service.GarageDoorOpener);
|
|
854
|
+
if (service) {
|
|
855
|
+
// UA Gate uses DPS state since it's a motorized gate with physical positions. Non-UA Gate hubs derive state from lock relay (locked = closed, unlocked = open)
|
|
856
|
+
// since GarageDoorOpener is just a visual convenience for the same underlying lock behavior.
|
|
857
|
+
if (this.uda.device_type === "UGT") {
|
|
858
|
+
const dpsState = isSideDoor ? this._hkSideDoorDpsState : this.hkDpsState;
|
|
859
|
+
const doorState = dpsState === this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED ? this.hap.Characteristic.CurrentDoorState.CLOSED :
|
|
860
|
+
this.hap.Characteristic.CurrentDoorState.OPEN;
|
|
861
|
+
const targetState = dpsState === this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED ?
|
|
862
|
+
this.hap.Characteristic.TargetDoorState.CLOSED : this.hap.Characteristic.TargetDoorState.OPEN;
|
|
863
|
+
// If in transition cooldown, ignore all DPS updates to let the gate stabilize. The gate sensor often bounces between open/closed during movement. We'll accept
|
|
864
|
+
// the final state once the cooldown expires.
|
|
865
|
+
if (Date.now() < transitionUntil) {
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
service.updateCharacteristic(this.hap.Characteristic.TargetDoorState, targetState);
|
|
869
|
+
service.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, doorState);
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
// Non-UA Gate hubs: derive from lock relay state.
|
|
873
|
+
const doorState = this.isLocked(lockState) ? this.hap.Characteristic.CurrentDoorState.CLOSED : this.hap.Characteristic.CurrentDoorState.OPEN;
|
|
874
|
+
const targetState = this.isLocked(lockState) ? this.hap.Characteristic.TargetDoorState.CLOSED : this.hap.Characteristic.TargetDoorState.OPEN;
|
|
875
|
+
service.updateCharacteristic(this.hap.Characteristic.TargetDoorState, targetState);
|
|
876
|
+
service.updateCharacteristic(this.hap.Characteristic.CurrentDoorState, doorState);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
// The primary door has no subtype. The side door (UA Gate only) uses a subtype to distinguish it.
|
|
882
|
+
const service = isSideDoor ? this.accessory.getServiceById(this.hap.Service.LockMechanism, AccessReservedNames.LOCK_DOOR_SIDE) :
|
|
883
|
+
this.accessory.getService(this.hap.Service.LockMechanism);
|
|
884
|
+
if (service) {
|
|
885
|
+
service.updateCharacteristic(this.hap.Characteristic.LockTargetState, lockState === this.hap.Characteristic.LockCurrentState.UNSECURED ?
|
|
886
|
+
this.hap.Characteristic.LockTargetState.UNSECURED : this.hap.Characteristic.LockTargetState.SECURED);
|
|
887
|
+
service.updateCharacteristic(this.hap.Characteristic.LockCurrentState, lockState);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
// Update the lock trigger switch if enabled.
|
|
891
|
+
this.accessory.getServiceById(this.hap.Service.Switch, triggerSubtype)?.updateCharacteristic(this.hap.Characteristic.On, lockState !== this.hap.Characteristic.LockCurrentState.SECURED);
|
|
892
|
+
}
|
|
893
|
+
// Return the current HomeKit side door lock state that we are tracking for this hub.
|
|
894
|
+
get hkSideDoorLockState() {
|
|
895
|
+
return this._hkSideDoorLockState;
|
|
896
|
+
}
|
|
897
|
+
// Set the current HomeKit side door lock state for this hub.
|
|
898
|
+
set hkSideDoorLockState(value) {
|
|
899
|
+
// If nothing is changed, we're done.
|
|
900
|
+
if (this.hkSideDoorLockState === value) {
|
|
397
901
|
return;
|
|
398
902
|
}
|
|
399
|
-
// Update the state
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
this.accessory.getServiceById(this.hap.Service.Switch, AccessReservedNames.SWITCH_LOCK_TRIGGER)?.updateCharacteristic(this.hap.Characteristic.On, this.hkLockState !== this.hap.Characteristic.LockCurrentState.SECURED);
|
|
903
|
+
// Update the lock state.
|
|
904
|
+
this._hkSideDoorLockState = value;
|
|
905
|
+
// Update the lock service state.
|
|
906
|
+
this.updateDoorServiceState(true);
|
|
404
907
|
}
|
|
405
908
|
// Return the current state of the DPS on the hub.
|
|
406
909
|
get hubDpsState() {
|
|
@@ -425,6 +928,16 @@ export class AccessHub extends AccessDevice {
|
|
|
425
928
|
return (this.uda.configs?.find(entry => entry.key === relayType)?.value === "on") ? this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED :
|
|
426
929
|
this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED;
|
|
427
930
|
}
|
|
931
|
+
// Return the current state of the side door DPS on the UA Gate hub.
|
|
932
|
+
get hubSideDoorDpsState() {
|
|
933
|
+
// If we don't have the wiring connected for the side door DPS, we report our default closed state.
|
|
934
|
+
if (!this.isSideDoorDpsWired) {
|
|
935
|
+
return this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED;
|
|
936
|
+
}
|
|
937
|
+
// Return our side door DPS state. If it's anything other than on, we assume it's open.
|
|
938
|
+
return (this.uda.configs?.find(entry => entry.key === "input_door_dps")?.value === "on") ? this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED :
|
|
939
|
+
this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED;
|
|
940
|
+
}
|
|
428
941
|
// Return the current state of the relay lock on the hub.
|
|
429
942
|
get hubLockState() {
|
|
430
943
|
let relayType;
|
|
@@ -443,6 +956,15 @@ export class AccessHub extends AccessDevice {
|
|
|
443
956
|
const lockRelay = this.uda.configs?.find(entry => entry.key === relayType);
|
|
444
957
|
return (lockRelay?.value === "off") ? this.hap.Characteristic.LockCurrentState.SECURED : this.hap.Characteristic.LockCurrentState.UNSECURED;
|
|
445
958
|
}
|
|
959
|
+
// Return the current state of the side door relay lock on the UA Gate hub.
|
|
960
|
+
get hubSideDoorLockState() {
|
|
961
|
+
// Side door lock is only available on UA Gate.
|
|
962
|
+
if (this.uda.device_type !== "UGT") {
|
|
963
|
+
return this.hap.Characteristic.LockCurrentState.SECURED;
|
|
964
|
+
}
|
|
965
|
+
const lockRelay = this.uda.configs?.find(entry => entry.key === "output_oper2_relay");
|
|
966
|
+
return (lockRelay?.value === "off") ? this.hap.Characteristic.LockCurrentState.SECURED : this.hap.Characteristic.LockCurrentState.UNSECURED;
|
|
967
|
+
}
|
|
446
968
|
// Return the current state of the REL on the hub.
|
|
447
969
|
get hubRelState() {
|
|
448
970
|
// If we don't have the wiring connected for the REL, we report our default closed state.
|
|
@@ -501,11 +1023,20 @@ export class AccessHub extends AccessDevice {
|
|
|
501
1023
|
return (this.uda.configs?.find(relay => relay.key === relayType)?.value === "on") ? this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED :
|
|
502
1024
|
this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED;
|
|
503
1025
|
}
|
|
1026
|
+
// Return the wiring state of the side door DPS on UA Gate hubs. The side door DPS uses different wiring keys than the main gate DPS.
|
|
1027
|
+
get isSideDoorDpsWired() {
|
|
1028
|
+
// Side door DPS is only available on UA Gate.
|
|
1029
|
+
if (this.uda.device_type !== "UGT") {
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
// Check if the side door DPS is wired (wiring_state_door-dps-neg and wiring_state_door-dps-pos).
|
|
1033
|
+
return ["wiring_state_door-dps-neg", "wiring_state_door-dps-pos"].every(wire => this.uda.configs?.some(e => (e.key === wire) && (e.value === "on")));
|
|
1034
|
+
}
|
|
504
1035
|
// Utility to check the wiring state of a given terminal input.
|
|
505
1036
|
isWired(input) {
|
|
506
1037
|
// UA-ULTRA proxies via button mode.
|
|
507
1038
|
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;
|
|
1039
|
+
return this.uda.extensions?.[0]?.target_config?.some(e => (e.config_key === "rex_button_mode") && (e.config_value === sensorWiring[input].proxyMode)) ?? false;
|
|
509
1040
|
}
|
|
510
1041
|
// Find the wiring keys for this model.
|
|
511
1042
|
const wires = sensorWiring[input].wiring?.[this.uda.device_type];
|
|
@@ -528,109 +1059,284 @@ export class AccessHub extends AccessDevice {
|
|
|
528
1059
|
hasCapability(capability) {
|
|
529
1060
|
return Array.isArray(capability) ? capability.some(c => this.uda.capabilities.includes(c)) : this.uda.capabilities.includes(capability);
|
|
530
1061
|
}
|
|
531
|
-
//
|
|
532
|
-
|
|
1062
|
+
// Update door state from location data (lock and DPS).
|
|
1063
|
+
updateDoorFromLocationState(doorState, isSideDoor) {
|
|
1064
|
+
const newLockState = this.toLockState(doorState.lock);
|
|
1065
|
+
const newDpsState = this.toDpsState(doorState.dps);
|
|
1066
|
+
const doorName = isSideDoor ? "Side door" : "";
|
|
1067
|
+
// Update lock state.
|
|
1068
|
+
const currentLockState = isSideDoor ? this.hkSideDoorLockState : this.hkLockState;
|
|
1069
|
+
if (newLockState !== currentLockState) {
|
|
1070
|
+
if (isSideDoor) {
|
|
1071
|
+
this.hkSideDoorLockState = newLockState;
|
|
1072
|
+
this.controller.mqtt?.publish(this.id, "sidedoor/lock", this.isLocked(newLockState) ? "true" : "false");
|
|
1073
|
+
if (this.hints.logLock) {
|
|
1074
|
+
this.log.info("%s %s.", doorName, this.isLocked(newLockState) ? "locked" : "unlocked");
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
else {
|
|
1078
|
+
this.hkLockState = newLockState;
|
|
1079
|
+
this.controller.mqtt?.publish(this.id, "lock", this.isLocked(newLockState) ? "true" : "false");
|
|
1080
|
+
if (this.hints.logLock) {
|
|
1081
|
+
this.log.info(this.isLocked(newLockState) ? "Locked." : "Unlocked.");
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
// Update DPS state.
|
|
1086
|
+
const currentDpsState = isSideDoor ? this._hkSideDoorDpsState : this.hkDpsState;
|
|
1087
|
+
if (newDpsState !== currentDpsState) {
|
|
1088
|
+
const contactDetected = this.isClosed(newDpsState);
|
|
1089
|
+
if (isSideDoor) {
|
|
1090
|
+
this._hkSideDoorDpsState = newDpsState;
|
|
1091
|
+
this.setContactSensorState(AccessReservedNames.CONTACT_DPS_SIDE, newDpsState);
|
|
1092
|
+
this.controller.mqtt?.publish(this.id, "sidedoor/dps", contactDetected ? "false" : "true");
|
|
1093
|
+
if (this.hints.logDps) {
|
|
1094
|
+
this.log.info("Side door position sensor %s.", contactDetected ? "closed" : "open");
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
else {
|
|
1098
|
+
this.hkDpsState = newDpsState;
|
|
1099
|
+
this.controller.mqtt?.publish(this.id, "dps", contactDetected ? "false" : "true");
|
|
1100
|
+
if ((this.doorServiceType === "GarageDoorOpener") || this.hints.logDps) {
|
|
1101
|
+
this.log.info("Door position sensor %s.", contactDetected ? "closed" : "open");
|
|
1102
|
+
}
|
|
1103
|
+
if (this.doorServiceType === "GarageDoorOpener") {
|
|
1104
|
+
this.updateDoorServiceState(false);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
// Handle remote unlock events.
|
|
1110
|
+
handleRemoteUnlock(packet) {
|
|
1111
|
+
// For UA Gate hubs, determine which door was unlocked based on the event_object_id.
|
|
1112
|
+
if (this.uda.device_type === "UGT") {
|
|
1113
|
+
const eventDoorId = packet.event_object_id;
|
|
1114
|
+
const isSideDoor = this.sideDoorLocationId && (eventDoorId === this.sideDoorLocationId);
|
|
1115
|
+
const isMainDoor = this.mainDoorLocationId && (eventDoorId === this.mainDoorLocationId);
|
|
1116
|
+
if (!isSideDoor && !isMainDoor) {
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
const doorName = isSideDoor ? "Side door" : "Gate";
|
|
1120
|
+
const mqttTopic = isSideDoor ? "sidedoor/lock" : "lock";
|
|
1121
|
+
// Set unlocked state.
|
|
1122
|
+
if (isSideDoor) {
|
|
1123
|
+
this.hkSideDoorLockState = this.hap.Characteristic.LockCurrentState.UNSECURED;
|
|
1124
|
+
}
|
|
1125
|
+
else {
|
|
1126
|
+
this.hkLockState = this.hap.Characteristic.LockCurrentState.UNSECURED;
|
|
1127
|
+
}
|
|
1128
|
+
this.controller.mqtt?.publish(this.id, mqttTopic, "false");
|
|
1129
|
+
if (this.hints.logLock) {
|
|
1130
|
+
this.log.info("%s unlocked.", doorName);
|
|
1131
|
+
}
|
|
1132
|
+
// Auto-lock after delay.
|
|
1133
|
+
setTimeout(() => {
|
|
1134
|
+
if (isSideDoor) {
|
|
1135
|
+
this.hkSideDoorLockState = this.hap.Characteristic.LockCurrentState.SECURED;
|
|
1136
|
+
}
|
|
1137
|
+
else {
|
|
1138
|
+
this.hkLockState = this.hap.Characteristic.LockCurrentState.SECURED;
|
|
1139
|
+
}
|
|
1140
|
+
this.controller.mqtt?.publish(this.id, mqttTopic, "true");
|
|
1141
|
+
if (this.hints.logLock) {
|
|
1142
|
+
this.log.info("%s locked.", doorName);
|
|
1143
|
+
}
|
|
1144
|
+
}, AUTO_LOCK_DELAY_MS);
|
|
1145
|
+
}
|
|
1146
|
+
else {
|
|
1147
|
+
// Non-UA Gate hubs: default behavior.
|
|
1148
|
+
this.hkLockState = this.hap.Characteristic.LockCurrentState.UNSECURED;
|
|
1149
|
+
this.controller.mqtt?.publish(this.id, "lock", "false");
|
|
1150
|
+
if (this.hints.logLock) {
|
|
1151
|
+
this.log.info("Unlocked.");
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
// Handle device update events (v1 API).
|
|
1156
|
+
handleDeviceUpdate(packet) {
|
|
533
1157
|
const terminalInputs = [
|
|
534
1158
|
{ input: "Dps", label: "Door position sensor", topic: "dps" },
|
|
535
1159
|
{ input: "Rel", label: "Remote release", topic: "rel" },
|
|
536
1160
|
{ input: "Ren", label: "Request to enter sensor", topic: "ren" },
|
|
537
1161
|
{ input: "Rex", label: "Request to exit sensor", topic: "rex" }
|
|
538
1162
|
];
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
this.
|
|
1163
|
+
// Process a lock update event if our state has changed. Skip for UA Gate hubs since we handle state manually because Access doesn't send those events out.
|
|
1164
|
+
if ((this.uda.device_type !== "UGT") && (this.hubLockState !== this.hkLockState)) {
|
|
1165
|
+
this.hkLockState = this.hubLockState;
|
|
1166
|
+
this.controller.mqtt?.publish(this.id, "lock", this.isLocked(this.hkLockState) ? "true" : "false");
|
|
1167
|
+
if (this.hints.logLock) {
|
|
1168
|
+
this.log.info(this.isLocked(this.hkLockState) ? "Locked." : "Unlocked.");
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
// Process a side door lock update event if our state has changed (UA Gate only). We skip this for UGT since polling handles state updates.
|
|
1172
|
+
if (this.hints.hasSideDoor && (this.uda.device_type !== "UGT")) {
|
|
1173
|
+
const currentHkState = this.hkSideDoorLockState;
|
|
1174
|
+
const newHubState = this.hubSideDoorLockState;
|
|
1175
|
+
if (newHubState !== currentHkState) {
|
|
1176
|
+
this.hkSideDoorLockState = newHubState;
|
|
1177
|
+
this.controller.mqtt?.publish(this.id, "sidedoor/lock", this.isLocked(this.hkSideDoorLockState) ? "true" : "false");
|
|
545
1178
|
if (this.hints.logLock) {
|
|
546
|
-
this.log.info("
|
|
1179
|
+
this.log.info("Side door %s.", this.isLocked(this.hkSideDoorLockState) ? "locked" : "unlocked");
|
|
547
1180
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
// Process a side door DPS update event if our state has changed (UA Gate only). We skip this for UGT since polling handles state updates.
|
|
1184
|
+
if (this.hints.hasSideDoor && this.hints.hasWiringDps && (this.uda.device_type !== "UGT")) {
|
|
1185
|
+
const newSideDoorDpsState = this.hubSideDoorDpsState;
|
|
1186
|
+
if (newSideDoorDpsState !== this._hkSideDoorDpsState) {
|
|
1187
|
+
this._hkSideDoorDpsState = newSideDoorDpsState;
|
|
1188
|
+
const contactDetected = this.isClosed(this._hkSideDoorDpsState);
|
|
1189
|
+
this.controller.mqtt?.publish(this.id, "sidedoor/dps", contactDetected ? "false" : "true");
|
|
1190
|
+
if (this.hints.logDps) {
|
|
1191
|
+
this.log.info("Side door position sensor %s.", contactDetected ? "closed" : "open");
|
|
557
1192
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
// Process any terminal input update events if our state has changed.
|
|
1196
|
+
for (const { input, topic, label } of terminalInputs) {
|
|
1197
|
+
const hasKey = ("hasWiring" + input);
|
|
1198
|
+
const hkKey = ("hk" + input + "State");
|
|
1199
|
+
const hubKey = ("hub" + input + "State");
|
|
1200
|
+
const logKey = ("log" + input);
|
|
1201
|
+
const wiredKey = ("is" + input + "Wired");
|
|
1202
|
+
if (this.hints[hasKey] && (this[hubKey] !== this[hkKey])) {
|
|
1203
|
+
this[hkKey] = this[hubKey];
|
|
1204
|
+
if (this[wiredKey]) {
|
|
1205
|
+
const contactDetected = this.isClosed(this[hkKey]);
|
|
1206
|
+
this.controller.mqtt?.publish(this.id, topic, contactDetected ? "false" : "true");
|
|
1207
|
+
if (this.hints[logKey]) {
|
|
1208
|
+
this.log.info("%s %s.", label, contactDetected ? "closed" : "open");
|
|
574
1209
|
}
|
|
575
1210
|
}
|
|
576
|
-
//
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
this.
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
1211
|
+
// When DPS state changes on UA Gate, update the GarageDoorOpener service state. Non-UA Gate hubs with GarageDoorOpener derive state from lock relay changes
|
|
1212
|
+
// instead, so DPS updates don't affect the door service.
|
|
1213
|
+
if ((input === "Dps") && (this.doorServiceType === "GarageDoorOpener") && (this.uda.device_type === "UGT")) {
|
|
1214
|
+
this.updateDoorServiceState(false);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
// Process any changes to terminal input configuration.
|
|
1219
|
+
if (packet.data.extensions?.[0]?.target_config && (this.uda.display_model === "UA Ultra")) {
|
|
1220
|
+
this.checkUltraInputs();
|
|
1221
|
+
this.configureTerminalInputs();
|
|
1222
|
+
}
|
|
1223
|
+
// Process any changes to our online status.
|
|
1224
|
+
if (packet.data.is_online !== undefined) {
|
|
1225
|
+
for (const sensor of Object.keys(AccessReservedNames).filter(key => key.startsWith("CONTACT_"))) {
|
|
1226
|
+
this.accessory.getServiceById(this.hap.Service.ContactSensor, AccessReservedNames[sensor])?.
|
|
1227
|
+
updateCharacteristic(this.hap.Characteristic.StatusActive, !!packet.data.is_online);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
// Handle device update v2 events.
|
|
1232
|
+
handleDeviceUpdateV2(packet) {
|
|
1233
|
+
const data = packet.data;
|
|
1234
|
+
// Process access method updates.
|
|
1235
|
+
if (data.access_method) {
|
|
1236
|
+
const accessMethodData = data.access_method;
|
|
1237
|
+
for (const [key, value] of Object.entries(accessMethodData)) {
|
|
1238
|
+
const accessMethod = accessMethods.find(entry => entry.key === key);
|
|
1239
|
+
if (accessMethod) {
|
|
1240
|
+
this.accessory.getServiceById(this.hap.Service.Switch, accessMethod.subtype)?.updateCharacteristic(this.hap.Characteristic.On, value === "yes");
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
// Process location_states for UA Gate hubs - this contains lock state per door.
|
|
1245
|
+
if (data.location_states && (this.uda.device_type === "UGT")) {
|
|
1246
|
+
const locationStates = data.location_states;
|
|
1247
|
+
// Process main door state.
|
|
1248
|
+
const mainDoorExtension = this.uda.extensions?.find(ext => ext.source_id === "port1");
|
|
1249
|
+
const mainDoorId = mainDoorExtension?.target_value ?? this.mainDoorLocationId;
|
|
1250
|
+
if (mainDoorId) {
|
|
1251
|
+
const mainDoorState = locationStates.find(state => state.location_id === mainDoorId);
|
|
1252
|
+
if (mainDoorState) {
|
|
1253
|
+
this.updateDoorFromLocationState(mainDoorState, false);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
// Process side door state.
|
|
1257
|
+
if (this.hints.hasSideDoor) {
|
|
1258
|
+
const sideDoorExtension = this.uda.extensions?.find(ext => ext.source_id === "port2");
|
|
1259
|
+
const sideDoorId = sideDoorExtension?.target_value ?? this.sideDoorLocationId;
|
|
1260
|
+
if (sideDoorId) {
|
|
1261
|
+
const sideDoorState = locationStates.find(state => state.location_id === sideDoorId);
|
|
1262
|
+
if (sideDoorState) {
|
|
1263
|
+
this.updateDoorFromLocationState(sideDoorState, true);
|
|
587
1264
|
}
|
|
588
1265
|
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
// Handle location update events (v2 API).
|
|
1270
|
+
handleLocationUpdate(packet) {
|
|
1271
|
+
// Only process for UA Gate hubs.
|
|
1272
|
+
if (this.uda.device_type !== "UGT") {
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
const locationData = packet.data;
|
|
1276
|
+
if (!locationData.state) {
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
const locationId = locationData.id;
|
|
1280
|
+
const isMainDoor = locationId === this.mainDoorLocationId;
|
|
1281
|
+
const isSideDoor = locationId === this.sideDoorLocationId;
|
|
1282
|
+
if (isMainDoor) {
|
|
1283
|
+
this.updateDoorFromLocationState(locationData.state, false);
|
|
1284
|
+
}
|
|
1285
|
+
else if (isSideDoor && this.hints.hasSideDoor) {
|
|
1286
|
+
this.updateDoorFromLocationState(locationData.state, true);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
// Handle doorbell ring events.
|
|
1290
|
+
handleDoorbellRing(packet) {
|
|
1291
|
+
if ((packet.data.connected_uah_id !== this.uda.unique_id) || !this.hasCapability("door_bell")) {
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
this.doorbellRingRequestId = packet.data.request_id;
|
|
1295
|
+
// Trigger the doorbell event in HomeKit.
|
|
1296
|
+
this.accessory.getService(this.hap.Service.Doorbell)?.getCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent)
|
|
1297
|
+
?.sendEventNotification(this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS);
|
|
1298
|
+
// Update our doorbell trigger, if needed.
|
|
1299
|
+
this.accessory.getServiceById(this.hap.Service.Switch, AccessReservedNames.SWITCH_DOORBELL_TRIGGER)?.updateCharacteristic(this.hap.Characteristic.On, true);
|
|
1300
|
+
// Publish to MQTT.
|
|
1301
|
+
this.controller.mqtt?.publish(this.id, "doorbell", "true");
|
|
1302
|
+
if (this.hints.logDoorbell) {
|
|
1303
|
+
this.log.info("Doorbell ring detected.");
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
// Handle doorbell cancel events.
|
|
1307
|
+
handleDoorbellCancel(packet) {
|
|
1308
|
+
if (this.doorbellRingRequestId !== packet.data.remote_call_request_id) {
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
this.doorbellRingRequestId = null;
|
|
1312
|
+
// Update our doorbell trigger, if needed.
|
|
1313
|
+
this.accessory.getServiceById(this.hap.Service.Switch, AccessReservedNames.SWITCH_DOORBELL_TRIGGER)?.updateCharacteristic(this.hap.Characteristic.On, false);
|
|
1314
|
+
// Publish to MQTT.
|
|
1315
|
+
this.controller.mqtt?.publish(this.id, "doorbell", "false");
|
|
1316
|
+
if (this.hints.logDoorbell) {
|
|
1317
|
+
this.log.info("Doorbell ring cancelled.");
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
// Handle hub-related events.
|
|
1321
|
+
eventHandler(packet) {
|
|
1322
|
+
switch (packet.event) {
|
|
1323
|
+
case "access.data.device.remote_unlock":
|
|
1324
|
+
this.handleRemoteUnlock(packet);
|
|
1325
|
+
break;
|
|
1326
|
+
case "access.data.device.update":
|
|
1327
|
+
this.handleDeviceUpdate(packet);
|
|
589
1328
|
break;
|
|
590
1329
|
case "access.data.v2.device.update":
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
const accessMethod = accessMethods.find(entry => entry.key === key);
|
|
596
|
-
if (!accessMethod) {
|
|
597
|
-
continue;
|
|
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");
|
|
601
|
-
}
|
|
602
|
-
}
|
|
1330
|
+
this.handleDeviceUpdateV2(packet);
|
|
1331
|
+
break;
|
|
1332
|
+
case "access.data.v2.location.update":
|
|
1333
|
+
this.handleLocationUpdate(packet);
|
|
603
1334
|
break;
|
|
604
1335
|
case "access.remote_view":
|
|
605
|
-
|
|
606
|
-
if ((packet.data.connected_uah_id !== this.uda.unique_id) || !this.hasCapability("door_bell")) {
|
|
607
|
-
break;
|
|
608
|
-
}
|
|
609
|
-
this.doorbellRingRequestId = packet.data.request_id;
|
|
610
|
-
// Trigger the doorbell event in HomeKit.
|
|
611
|
-
this.accessory.getService(this.hap.Service.Doorbell)?.getCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent)
|
|
612
|
-
?.sendEventNotification(this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS);
|
|
613
|
-
// Update our doorbell trigger, if needed.
|
|
614
|
-
this.accessory.getServiceById(this.hap.Service.Switch, AccessReservedNames.SWITCH_DOORBELL_TRIGGER)?.updateCharacteristic(this.hap.Characteristic.On, true);
|
|
615
|
-
// Publish to MQTT, if configured to do so.
|
|
616
|
-
this.controller.mqtt?.publish(this.id, "doorbell", "true");
|
|
617
|
-
if (this.hints.logDoorbell) {
|
|
618
|
-
this.log.info("Doorbell ring detected.");
|
|
619
|
-
}
|
|
1336
|
+
this.handleDoorbellRing(packet);
|
|
620
1337
|
break;
|
|
621
1338
|
case "access.remote_view.change":
|
|
622
|
-
|
|
623
|
-
if (this.doorbellRingRequestId !== packet.data.remote_call_request_id) {
|
|
624
|
-
break;
|
|
625
|
-
}
|
|
626
|
-
this.doorbellRingRequestId = null;
|
|
627
|
-
// Update our doorbell trigger, if needed.
|
|
628
|
-
this.accessory.getServiceById(this.hap.Service.Switch, AccessReservedNames.SWITCH_DOORBELL_TRIGGER)?.updateCharacteristic(this.hap.Characteristic.On, false);
|
|
629
|
-
// Publish to MQTT, if configured to do so.
|
|
630
|
-
this.controller.mqtt?.publish(this.id, "doorbell", "false");
|
|
631
|
-
if (this.hints.logDoorbell) {
|
|
632
|
-
this.log.info("Doorbell ring cancelled.");
|
|
633
|
-
}
|
|
1339
|
+
this.handleDoorbellCancel(packet);
|
|
634
1340
|
break;
|
|
635
1341
|
default:
|
|
636
1342
|
break;
|
|
@@ -639,8 +1345,9 @@ export class AccessHub extends AccessDevice {
|
|
|
639
1345
|
// 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
1346
|
// against whatever Ubiquiti may do in the future given the inconsistencies in their API implementation for Access across devices of even similar types.
|
|
641
1347
|
static {
|
|
642
|
-
// We define the specific sensor input properties we need.
|
|
643
|
-
|
|
1348
|
+
// We define the specific sensor input properties we need. We skip DPS since we implement it with a manual getter/setter that provides fallback behavior when the
|
|
1349
|
+
// DPS contact sensor is disabled.
|
|
1350
|
+
for (const input of sensorInputs.filter(i => i !== "Dps")) {
|
|
644
1351
|
let propName = "hk" + input + "State";
|
|
645
1352
|
const enumKey = "CONTACT_" + input.toUpperCase();
|
|
646
1353
|
Object.defineProperty(AccessHub.prototype, propName, {
|