homebridge-unifi-access
Version:
Homebridge UniFi Access plugin providing complete HomeKit integration for the UniFi Access ecosystem with full support for most features including autoconfiguration, motion detection, multiple controllers, and realtime updates.
402 lines • 20.9 kB
JavaScript
import { acquireService, validService } from "homebridge-plugin-utils";
import { AccessDevice } from "./access-device.js";
import { AccessReservedNames } from "./access-types.js";
export class AccessHub extends AccessDevice {
_hkLockState;
doorbellRingRequestId;
lockDelayInterval;
uda;
// Create an instance.
constructor(controller, device, accessory) {
super(controller, accessory);
this.uda = device;
this._hkLockState = this.hubLockState;
this.lockDelayInterval = this.getFeatureNumber("Hub.LockDelayInterval") ?? undefined;
this.doorbellRingRequestId = null;
// If we attempt to set the delay interval to something invalid, then assume we are using the default unlock behavior.
if ((this.lockDelayInterval !== undefined) && (this.lockDelayInterval < 0)) {
this.lockDelayInterval = undefined;
}
this.configureHints();
this.configureDevice();
}
// Configure device-specific settings for this device.
configureHints() {
// Configure our parent's hints.
super.configureHints();
this.hints.hasDps = this.hasCapability(["dps_alarm", "dps_mode_selectable", "dps_trigger_level"]) && this.hasFeature("Hub.DPS");
this.hints.logDoorbell = this.hasFeature("Log.Doorbell");
this.hints.logDps = this.hasFeature("Log.DPS");
this.hints.logLock = this.hasFeature("Log.Lock");
return true;
}
// Initialize and configure the light accessory for HomeKit.
configureDevice() {
this._hkLockState = this.hubLockState;
// Clean out the context object in case it's been polluted somehow.
this.accessory.context = {};
this.accessory.context.mac = this.uda.mac;
this.accessory.context.controller = this.controller.uda.host.mac;
if (this.lockDelayInterval === undefined) {
this.log.info("The door lock relay will lock five seconds after unlocking in HomeKit.");
}
else {
this.log.info("The door lock relay will remain unlocked %s after unlocking in HomeKit.", this.lockDelayInterval === 0 ? "indefinitely" : "for " + this.lockDelayInterval.toString() + " minutes");
}
// Configure accessory information.
this.configureInfo();
// Configure the lock.
this.configureLock();
this.configureLockTrigger();
// Configure the doorbell.
this.configureDoorbell();
this.configureDoorbellTrigger();
// Configure the door position sensor.
this.configureDps();
// Configure MQTT services.
this.configureMqtt();
// Listen for events.
this.controller.events.on(this.uda.unique_id, this.listeners[this.uda.unique_id] = this.eventHandler.bind(this));
this.controller.events.on("access.remote_view", this.listeners["access.remote_view"] = this.eventHandler.bind(this));
this.controller.events.on("access.remote_view.change", this.listeners["access.remote_view.change"] = this.eventHandler.bind(this));
return true;
}
// Configure the doorbell service for HomeKit.
configureDoorbell() {
// Validate whether we should have this service enabled.
if (!validService(this.accessory, this.hap.Service.Doorbell, this.hasCapability("door_bell") && this.hasFeature("Hub.Doorbell"))) {
return false;
}
// Acquire the service.
const service = acquireService(this.hap, this.accessory, this.hap.Service.Doorbell, this.accessoryName, undefined, () => this.log.info("Enabling the doorbell."));
if (!service) {
this.log.error("Unable to add the doorbell.");
return false;
}
service.setPrimaryService(true);
return true;
}
// Configure the door position sensor for HomeKit.
configureDps() {
// Validate whether we should have this service enabled.
if (!validService(this.accessory, this.hap.Service.ContactSensor, this.hints.hasDps, AccessReservedNames.CONTACT_DPS)) {
return false;
}
// Acquire the service.
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."));
if (!service) {
this.log.error("Unable to add the door position sensor.");
return false;
}
// Initialize the light.
service.updateCharacteristic(this.hap.Characteristic.ContactSensorState, this.hubDpsState);
return true;
}
// Configure the lock for HomeKit.
configureLock() {
// Acquire the service.
const service = acquireService(this.hap, this.accessory, this.hap.Service.LockMechanism, this.accessoryName);
if (!service) {
this.log.error("Unable to add the lock.");
return false;
}
// Return the lock state.
service.getCharacteristic(this.hap.Characteristic.LockCurrentState)?.onGet(() => this.hkLockState);
service.getCharacteristic(this.hap.Characteristic.LockTargetState)?.onSet(async (value) => {
if (!(await this.hubLockCommand(value === this.hap.Characteristic.LockTargetState.SECURED))) {
// Revert our target state.
setTimeout(() => service.updateCharacteristic(this.hap.Characteristic.LockTargetState, !value), 50);
}
service.updateCharacteristic(this.hap.Characteristic.LockCurrentState, this.hkLockState);
});
// Initialize the lock.
this._hkLockState = -1;
service.displayName = this.accessoryName;
service.updateCharacteristic(this.hap.Characteristic.Name, this.accessoryName);
this.hkLockState = this.hubLockState;
service.setPrimaryService(true);
return true;
}
// Configure a switch to manually trigger a doorbell ring event for HomeKit.
configureDoorbellTrigger() {
// Validate whether we should have this service enabled.
if (!validService(this.accessory, this.hap.Service.Switch, this.hasCapability("door_bell") && this.hasFeature("Hub.Doorbell.Trigger"), AccessReservedNames.SWITCH_DOORBELL_TRIGGER)) {
return false;
}
// Acquire the service.
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."));
if (!service) {
this.log.error("Unable to add the doorbell automation trigger.");
return false;
}
// Trigger the doorbell.
service.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => this.doorbellRingRequestId !== null);
// The state isn't really user-triggerable. We have no way, currently, to trigger a ring event on the hub.
service.getCharacteristic(this.hap.Characteristic.On)?.onSet(() => {
setTimeout(() => service.updateCharacteristic(this.hap.Characteristic.On, this.doorbellRingRequestId !== null), 50);
});
// Initialize the switch.
service.updateCharacteristic(this.hap.Characteristic.ConfiguredName, this.accessoryName + " Doorbell Trigger");
service.updateCharacteristic(this.hap.Characteristic.On, false);
return true;
}
// 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.
configureLockTrigger() {
// Validate whether we should have this service enabled.
if (!validService(this.accessory, this.hap.Service.Switch, this.hasFeature("Hub.Lock.Trigger"), AccessReservedNames.SWITCH_LOCK_TRIGGER)) {
return false;
}
// Acquire the service.
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."));
if (!service) {
this.log.error("Unable to add the lock automation trigger.");
return false;
}
// Trigger the doorbell.
service.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => this.hkLockState !== this.hap.Characteristic.LockCurrentState.SECURED);
// The state isn't really user-triggerable. We have no way, currently, to trigger a lock or unlock event on the hub.
service.getCharacteristic(this.hap.Characteristic.On)?.onSet(async (value) => {
// If we are on, we are in an unlocked state. If we are off, we are in a locked state.
if (!(await this.hubLockCommand(!value))) {
// Revert our state.
setTimeout(() => service.updateCharacteristic(this.hap.Characteristic.On, !value), 50);
}
});
// Initialize the switch.
service.updateCharacteristic(this.hap.Characteristic.ConfiguredName, this.accessoryName + " Lock Trigger");
service.updateCharacteristic(this.hap.Characteristic.On, false);
return true;
}
// Configure MQTT capabilities of this light.
configureMqtt() {
const lockService = this.accessory.getService(this.hap.Service.LockMechanism);
if (!lockService) {
return false;
}
// MQTT doorbell status.
this.controller.mqtt?.subscribeGet(this.id, "doorbell", "Doorbell ring", () => {
return this.doorbellRingRequestId !== null ? "true" : "false";
});
// MQTT DPS status.
this.controller.mqtt?.subscribeGet(this.id, "dps", "Door position sensor", () => {
if (!this.isDpsWired) {
return "unknown";
}
switch (this.hkDpsState) {
case this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED:
return "false";
case this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED:
return "true";
default:
return "unknown";
}
});
// MQTT lock status.
this.controller.mqtt?.subscribeGet(this.id, "lock", "Lock", () => {
switch (this.hkLockState) {
case this.hap.Characteristic.LockCurrentState.SECURED:
return "true";
case this.hap.Characteristic.LockCurrentState.UNSECURED:
return "false";
default:
return "unknown";
}
});
// MQTT lock status.
this.controller.mqtt?.subscribeSet(this.id, "lock", "Lock", (value) => {
switch (value) {
case "true":
void this.controller.udaApi.unlock(this.uda, 0);
break;
case "false":
void this.controller.udaApi.unlock(this.uda, Infinity);
break;
default:
this.log.error("MQTT: Unknown lock set message received: %s.", value);
break;
}
});
return true;
}
// Utility function to execute lock and unlock actions on a hub.
async hubLockCommand(isLocking) {
const action = isLocking ? "lock" : "unlock";
// Only allow relocking if we are able to do so.
if ((this.lockDelayInterval === undefined) && isLocking) {
this.log.error("Unable to manually relock when the lock relay is configured to the default settings.");
return false;
}
// If we're not online, we're done.
if (!this.isOnline) {
this.log.error("Unable to %s. Device is offline.", action);
return false;
}
// Execute the action.
if (!(await this.controller.udaApi.unlock(this.uda, (this.lockDelayInterval === undefined) ? undefined : (isLocking ? 0 : Infinity)))) {
this.log.error("Unable to %s.", action);
return false;
}
return true;
}
// Return the current HomeKit DPS state that we are tracking for this hub.
get hkDpsState() {
return this.accessory.getService(this.hap.Service.ContactSensor)?.getCharacteristic(this.hap.Characteristic.ContactSensorState).value ??
this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED;
}
// Set the current HomeKit DPS state for this hub.
set hkDpsState(value) {
// Update the state of the contact service.
this.accessory.getService(this.hap.Service.ContactSensor)?.updateCharacteristic(this.hap.Characteristic.ContactSensorState, value);
}
// Return the current HomeKit lock state that we are tracking for this hub.
get hkLockState() {
return this._hkLockState;
}
// Set the current HomeKit lock state for this hub.
set hkLockState(value) {
// If nothing is changed, we're done.
if (this.hkLockState === value) {
return;
}
// Update the lock state.
this._hkLockState = value;
// Retrieve the lock service.
const lockService = this.accessory.getService(this.hap.Service.LockMechanism);
if (!lockService) {
return;
}
// Update the state in HomeKit.
lockService.updateCharacteristic(this.hap.Characteristic.LockTargetState, this.hkLockState === this.hap.Characteristic.LockCurrentState.UNSECURED ?
this.hap.Characteristic.LockTargetState.UNSECURED : this.hap.Characteristic.LockTargetState.SECURED);
lockService.updateCharacteristic(this.hap.Characteristic.LockCurrentState, this.hkLockState);
this.accessory.getServiceById(this.hap.Service.Switch, AccessReservedNames.SWITCH_LOCK_TRIGGER)?.updateCharacteristic(this.hap.Characteristic.On, this.hkLockState !== this.hap.Characteristic.LockCurrentState.SECURED);
}
// Return the current state of the DPS on the hub.
get hubDpsState() {
// If we don't have the wiring connected for the DPS, we report our default closed state.
if (!this.isDpsWired) {
return this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED;
}
let relayType;
switch (this.uda.device_type) {
case "UA-Hub-Door-Mini":
case "UA-ULTRA":
relayType = "input_d1_dps";
break;
default:
relayType = "input_state_dps";
break;
}
// Return our DPS state. If it's anything other than on, we assume it's open.
return (this.uda.configs?.find(x => x.key === relayType)?.value === "on") ? this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED :
this.hap.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED;
}
// Return the current state of the relay lock on the hub.
get hubLockState() {
let relayType;
switch (this.uda.device_type) {
case "UA-Hub-Door-Mini":
case "UA-ULTRA":
relayType = "output_d1_lock_relay";
break;
default:
relayType = "input_state_rly-lock_dry";
break;
}
const lockRelay = this.uda.configs?.find(x => x.key === relayType);
return ((lockRelay?.value === "off") ? this.hap.Characteristic.LockCurrentState.SECURED : this.hap.Characteristic.LockCurrentState.UNSECURED) ??
this.hap.Characteristic.LockCurrentState.UNKNOWN;
}
// Return whether the DPS has been wired on the hub.
get isDpsWired() {
let wiringType = [];
switch (this.uda.device_type) {
case "UA-Hub-Door-Mini":
wiringType = ["wiring_state_d1-dps-neg", "wiring_state_d1-dps-pos"];
break;
case "UAH":
wiringType = ["wiring_state_dps-neg", "wiring_state_dps-pos"];
break;
case "UA-ULTRA":
return true;
default:
// By default, let's assume the wiring is not there.
return false;
}
// The DPS is considered wired only if all associated wiring is connected.
return wiringType.filter(wire => this.uda.configs?.some(x => x.key === wire && x.value === "on")).length === wiringType.length;
}
// Utility to validate hub capabilities.
hasCapability(capability) {
return Array.isArray(capability) ? capability.some(c => this.uda?.capabilities?.includes(c)) : this.uda?.capabilities?.includes(capability);
}
// Handle hub-related events.
eventHandler(packet) {
switch (packet.event) {
case "access.data.device.remote_unlock":
// Process an Access unlock event.
this.hkLockState = this.hap.Characteristic.LockCurrentState.UNSECURED;
// Publish to MQTT, if configured to do so.
this.controller.mqtt?.publish(this.id, "lock", "false");
if (this.hints.logLock) {
this.log.info("Unlocked.");
}
break;
case "access.data.device.update":
// Process a lock update event if our state has changed.
if (this.hubLockState !== this.hkLockState) {
this.hkLockState = this.hubLockState;
this.controller.mqtt?.publish(this.id, "lock", this.hkLockState === this.hap.Characteristic.LockCurrentState.SECURED ? "true" : "false");
if (this.hints.logLock) {
this.log.info(this.hkLockState === this.hap.Characteristic.LockCurrentState.SECURED ? "Locked." : "Unlocked.");
}
}
// Process a DPS update event if our state has changed.
if (this.hints.hasDps && (this.hubDpsState !== this.hkDpsState)) {
this.hkDpsState = this.hubDpsState;
// Publish to MQTT, if configured to do so.
if (this.isDpsWired) {
this.controller.mqtt?.publish(this.id, "dps", (this.hkDpsState === this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED) ? "false" : "true");
if (this.hints.logDps) {
this.log.info("Door position sensor " + ((this.hkDpsState === this.hap.Characteristic.ContactSensorState.CONTACT_DETECTED) ? "closed." : "open."));
}
}
}
break;
case "access.remote_view":
// Process an Access ring event if we're the intended target.
if ((packet.data.connected_uah_id !== this.uda.unique_id) || !this.hasCapability("door_bell")) {
break;
}
this.doorbellRingRequestId = packet.data.request_id;
// Trigger the doorbell event in HomeKit.
this.accessory.getService(this.hap.Service.Doorbell)?.getCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent)
?.sendEventNotification(this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS);
// Update our doorbell trigger, if needed.
this.accessory.getServiceById(this.hap.Service.Switch, AccessReservedNames.SWITCH_DOORBELL_TRIGGER)?.updateCharacteristic(this.hap.Characteristic.On, true);
// Publish to MQTT, if configured to do so.
this.controller.mqtt?.publish(this.id, "doorbell", "true");
if (this.hints.logDoorbell) {
this.log.info("Doorbell ring detected.");
}
break;
case "access.remote_view.change":
// Process the cancellation of an Access ring event if we're the intended target.
if (this.doorbellRingRequestId !== packet.data.remote_call_request_id) {
break;
}
this.doorbellRingRequestId = null;
// Update our doorbell trigger, if needed.
this.accessory.getServiceById(this.hap.Service.Switch, AccessReservedNames.SWITCH_DOORBELL_TRIGGER)?.updateCharacteristic(this.hap.Characteristic.On, false);
// Publish to MQTT, if configured to do so.
this.controller.mqtt?.publish(this.id, "doorbell", "false");
if (this.hints.logDoorbell) {
this.log.info("Doorbell ring cancelled.");
}
break;
default:
break;
}
}
}
//# sourceMappingURL=access-hub.js.map