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.
364 lines • 18.9 kB
JavaScript
/* Copyright(C) 2017-2025, HJD (https://github.com/hjdhjd). All rights reserved.
*
* access-device.ts: Base class for all UniFi Access devices.
*/
import { ACCESS_MOTION_DURATION, ACCESS_OCCUPANCY_DURATION } from "./settings.js";
import { validateName } from "homebridge-plugin-utils";
import { AccessReservedNames } from "./access-types.js";
import util from "node:util";
export class AccessBase {
api;
debug;
hap;
log;
controller;
udaApi;
platform;
// The constructor initializes key variables and calls configureDevice().
constructor(controller) {
this.api = controller.platform.api;
this.debug = controller.platform.debug.bind(this);
this.hap = this.api.hap;
this.controller = controller;
this.udaApi = controller.udaApi;
this.platform = controller.platform;
this.log = {
debug: (message, ...parameters) => controller.platform.debug(util.format(this.name + ": " + message, ...parameters)),
error: (message, ...parameters) => controller.platform.log.error(util.format(this.name + ": " + message, ...parameters)),
info: (message, ...parameters) => controller.platform.log.info(util.format(this.name + ": " + message, ...parameters)),
warn: (message, ...parameters) => controller.platform.log.warn(util.format(this.name + ": " + message, ...parameters))
};
}
// Configure the device information for HomeKit.
setInfo(accessory, device) {
// If we don't have a device, we're done.
if (!device) {
return false;
}
// Update the manufacturer information for this device.
accessory.getService(this.hap.Service.AccessoryInformation)?.updateCharacteristic(this.hap.Characteristic.Manufacturer, "Ubiquiti Inc.");
// Update the model information for this device.
const deviceModel = device.display_model ?? device.model;
if (deviceModel.length) {
accessory.getService(this.hap.Service.AccessoryInformation)?.updateCharacteristic(this.hap.Characteristic.Model, deviceModel);
}
// Update the serial number for this device.
if (device.mac?.length) {
accessory.getService(this.hap.Service.AccessoryInformation)?.updateCharacteristic(this.hap.Characteristic.SerialNumber, device.mac.replace(/:/g, "").toUpperCase());
}
// Update the firmware revision for this device.
if (device.firmware?.length) {
// Capture the version of the device firmware, ensuring we get major, minor, and patch levels if they exist.
const versionRegex = /^v(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(.+))?$/;
const match = versionRegex.exec(device.firmware);
// Update our firmware revision.
accessory.getService(this.hap.Service.AccessoryInformation)?.updateCharacteristic(this.hap.Characteristic.FirmwareRevision, match ? match[1] + "." + (match[2] ?? "0") + "." + (match[3] ?? "0") : device.firmware);
}
return true;
}
// Utility function to return the fully enumerated name of this device.
get name() {
return this.controller.udaApi.name;
}
}
export class AccessDevice extends AccessBase {
accessory;
hints;
listeners;
// The constructor initializes key variables and calls configureDevice().
constructor(controller, accessory) {
// Call the constructor of our base class.
super(controller);
this.hints = {};
this.listeners = {};
// Set the accessory, if we have it. Otherwise, we expect configureDevice to assign it.
if (accessory) {
this.accessory = accessory;
}
}
// Configure device-specific settings.
configureHints() {
this.hints.logMotion = this.hasFeature("Log.Motion");
this.hints.motionDuration = this.getFeatureNumber("Motion.Duration") ?? ACCESS_MOTION_DURATION;
this.hints.occupancyDuration = this.getFeatureNumber("Motion.OccupancySensor.Duration") ?? ACCESS_OCCUPANCY_DURATION;
this.hints.syncName = this.hasFeature("Device.SyncName");
// Sanity check motion detection duration. Make sure it's never less than 2 seconds so we can actually alert the user.
if (this.hints.motionDuration < 2) {
this.hints.motionDuration = 2;
}
// Sanity check occupancy detection duration. Make sure it's never less than 60 seconds so we can actually alert the user.
if (this.hints.occupancyDuration < 60) {
this.hints.occupancyDuration = 60;
}
// Inform the user if we've opted for something other than the defaults.
if (this.hints.syncName) {
this.log.info("Syncing Access device name to HomeKit.");
}
if (this.hints.motionDuration !== ACCESS_MOTION_DURATION) {
this.log.info("Motion event duration set to %s seconds.", this.hints.motionDuration);
}
if (this.hints.occupancyDuration !== ACCESS_OCCUPANCY_DURATION) {
this.log.info("Occupancy event duration set to %s seconds.", this.hints.occupancyDuration);
}
return true;
}
// Configure the device information details for HomeKit.
configureInfo() {
// Sync the Access name with HomeKit, if configured.
if (this.hints.syncName) {
this.accessoryName = this.uda.alias;
}
return this.setInfo(this.accessory, this.uda);
}
// Cleanup our event handlers and any other activities as needed.
cleanup() {
for (const eventName of Object.keys(this.listeners)) {
this.controller.events.removeListener(eventName, this.listeners[eventName]);
delete this.listeners[eventName];
}
}
// Configure the Access motion sensor for HomeKit.
configureMotionSensor(isEnabled = true, isInitialized = false) {
// Find the motion sensor service, if it exists.
let motionService = this.accessory.getService(this.hap.Service.MotionSensor);
// Have we disabled the motion sensor?
if (!isEnabled) {
if (motionService) {
this.accessory.removeService(motionService);
this.controller.mqtt?.unsubscribe(this.id, "motion/trigger");
this.log.info("Disabling motion sensor.");
}
this.configureMotionSwitch(isEnabled);
this.configureMotionTrigger(isEnabled);
return false;
}
// We don't have a motion sensor, let's add it to the device.
if (!motionService) {
// We don't have it, add the motion sensor to the device.
motionService = new this.hap.Service.MotionSensor(this.accessoryName);
if (!motionService) {
this.log.error("Unable to add motion sensor.");
return false;
}
this.accessory.addService(motionService);
isInitialized = false;
this.log.info("Enabling motion sensor.");
}
// Have we previously initialized this sensor? We assume not by default, but this allows for scenarios where you may be dynamically reconfiguring a sensor at
// runtime (e.g. UniFi sensors can be reconfigured for various sensor modes in realtime).
if (!isInitialized) {
// Initialize the state of the motion sensor.
motionService.displayName = this.accessoryName;
motionService.updateCharacteristic(this.hap.Characteristic.Name, this.accessoryName);
motionService.updateCharacteristic(this.hap.Characteristic.MotionDetected, false);
motionService.updateCharacteristic(this.hap.Characteristic.StatusActive, this.isOnline);
motionService.getCharacteristic(this.hap.Characteristic.StatusActive).onGet(() => {
return this.isOnline;
});
// Configure our MQTT support.
this.configureMqttMotionTrigger();
// Configure any motion switches or triggers the user may have enabled or disabled.
this.configureMotionSwitch(isEnabled);
this.configureMotionTrigger(isEnabled);
}
return true;
}
// Configure a switch to easily activate or deactivate motion sensor detection for HomeKit.
configureMotionSwitch(isEnabled = true) {
// Find the switch service, if it exists.
let switchService = this.accessory.getServiceById(this.hap.Service.Switch, AccessReservedNames.SWITCH_MOTION_SENSOR);
// Motion switches are disabled by default unless the user enables them.
if (!isEnabled || !this.hasFeature("Motion.Switch")) {
if (switchService) {
this.accessory.removeService(switchService);
}
// If we disable the switch, make sure we fully reset it's state. Otherwise, we can end up in a situation (e.g. liveview switches) where we have
// disabled motion detection with no meaningful way to enable it again.
this.accessory.context.detectMotion = true;
return false;
}
this.log.info("Enabling motion sensor switch.");
const switchName = this.accessoryName + " Motion Events";
// Add the switch to the device, if needed.
if (!switchService) {
switchService = new this.hap.Service.Switch(switchName, AccessReservedNames.SWITCH_MOTION_SENSOR);
if (!switchService) {
this.log.error("Unable to add motion sensor switch.");
return false;
}
switchService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName);
this.accessory.addService(switchService);
}
// Activate or deactivate motion detection.
switchService.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => {
return this.accessory.context.detectMotion === true;
});
switchService.getCharacteristic(this.hap.Characteristic.On)?.onSet((value) => {
if (this.accessory.context.detectMotion !== value) {
this.log.info("Motion detection %s.", (value === true) ? "enabled" : "disabled");
}
this.accessory.context.detectMotion = value === true;
});
// Initialize the switch state.
if (!("detectMotion" in this.accessory.context)) {
this.accessory.context.detectMotion = true;
}
switchService.updateCharacteristic(this.hap.Characteristic.ConfiguredName, switchName);
switchService.updateCharacteristic(this.hap.Characteristic.On, this.accessory.context.detectMotion);
return true;
}
// Configure a switch to manually trigger a motion sensor event for HomeKit.
configureMotionTrigger(isEnabled = true) {
// Find the switch service, if it exists.
let triggerService = this.accessory.getServiceById(this.hap.Service.Switch, AccessReservedNames.SWITCH_MOTION_TRIGGER);
// Motion triggers are disabled by default and primarily exist for automation purposes.
if (!isEnabled || !this.hasFeature("Motion.Trigger")) {
if (triggerService) {
this.accessory.removeService(triggerService);
}
return false;
}
const triggerName = this.accessoryName + " Motion Trigger";
// Add the switch to the device, if needed.
if (!triggerService) {
triggerService = new this.hap.Service.Switch(triggerName, AccessReservedNames.SWITCH_MOTION_TRIGGER);
if (!triggerService) {
this.log.error("Unable to add motion sensor trigger.");
return false;
}
triggerService.addOptionalCharacteristic(this.hap.Characteristic.ConfiguredName);
this.accessory.addService(triggerService);
}
const motionService = this.accessory.getService(this.hap.Service.MotionSensor);
const switchService = this.accessory.getServiceById(this.hap.Service.Switch, AccessReservedNames.SWITCH_MOTION_SENSOR);
// Activate or deactivate motion detection.
triggerService.getCharacteristic(this.hap.Characteristic.On)?.onGet(() => {
return motionService?.getCharacteristic(this.hap.Characteristic.MotionDetected).value === true;
});
triggerService.getCharacteristic(this.hap.Characteristic.On)?.onSet((isOn) => {
if (isOn) {
// Check to see if motion events are disabled.
if (switchService && !switchService.getCharacteristic(this.hap.Characteristic.On).value) {
setTimeout(() => {
triggerService?.updateCharacteristic(this.hap.Characteristic.On, false);
}, 50);
}
else {
// Trigger the motion event.
this.controller.events.motionEventHandler(this);
// Inform the user.
this.log.info("Motion event triggered.");
}
return;
}
// If the motion sensor is still on, we should be as well.
if (motionService?.getCharacteristic(this.hap.Characteristic.MotionDetected).value) {
setTimeout(() => {
triggerService?.updateCharacteristic(this.hap.Characteristic.On, true);
}, 50);
}
});
// Initialize the switch.
triggerService.updateCharacteristic(this.hap.Characteristic.ConfiguredName, triggerName);
triggerService.updateCharacteristic(this.hap.Characteristic.On, false);
this.log.info("Enabling motion sensor automation trigger.");
return true;
}
// Configure MQTT motion triggers.
configureMqttMotionTrigger() {
// Trigger a motion event in MQTT, if requested to do so.
this.controller.mqtt?.subscribe(this.id, "motion/trigger", (message) => {
const value = message.toString();
// When we get the right message, we trigger the motion event.
if (value?.toLowerCase() !== "true") {
return;
}
// Trigger the motion event.
this.controller.events.motionEventHandler(this);
this.log.info("Motion event triggered via MQTT.");
});
return true;
}
// Configure the Access occupancy sensor for HomeKit.
configureOccupancySensor(isEnabled = true, isInitialized = false) {
// Find the occupancy sensor service, if it exists.
let occupancyService = this.accessory.getService(this.hap.Service.OccupancySensor);
// Occupancy sensors are disabled by default and primarily exist for automation purposes.
if (!isEnabled || !this.hasFeature("Motion.OccupancySensor")) {
if (occupancyService) {
this.accessory.removeService(occupancyService);
this.log.info("Disabling occupancy sensor.");
}
return false;
}
// We don't have an occupancy sensor, let's add it to the device.
if (!occupancyService) {
// We don't have it, add the occupancy sensor to the device.
occupancyService = new this.hap.Service.OccupancySensor(this.accessoryName);
if (!occupancyService) {
this.log.error("Unable to add occupancy sensor.");
return false;
}
this.accessory.addService(occupancyService);
}
// Have we previously initialized this sensor? We assume not by default, but this allows for scenarios where you may be dynamically reconfiguring a sensor at
// runtime (e.g. UniFi sensors can be reconfigured for various sensor modes in realtime).
if (!isInitialized) {
// Initialize the state of the occupancy sensor.
occupancyService.updateCharacteristic(this.hap.Characteristic.OccupancyDetected, false);
occupancyService.updateCharacteristic(this.hap.Characteristic.StatusActive, this.isOnline);
occupancyService.getCharacteristic(this.hap.Characteristic.StatusActive).onGet(() => {
return this.isOnline;
});
this.log.info("Enabling occupancy sensor.");
}
return true;
}
// Utility function to return a floating point configuration parameter on a device.
getFeatureFloat(option) {
return this.platform.featureOptions.getFloat(option, this.id, this.controller.id);
}
// Utility function to return an integer configuration parameter on a device.
getFeatureNumber(option) {
return this.platform.featureOptions.getInteger(option, this.id, this.controller.id);
}
// Utility function to return a configuration parameter on a device.
getFeatureValue(option) {
return this.platform.featureOptions.value(option, this.id, this.controller.id);
}
// Utility for checking feature options on a device.
hasFeature(option) {
return this.controller.hasFeature(option, this.id);
}
// Utility function for reserved identifiers for switches.
isReservedName(name) {
return name === undefined ? false : Object.values(AccessReservedNames).map(x => x.toUpperCase()).includes(name.toUpperCase());
}
// Utility function to determine whether or not a device is currently online.
get isOnline() {
return ["is_adopted", "is_connected", "is_managed", "is_online"].every(key => this.uda[key]);
}
// Return a unique identifier for an Access device.
get id() {
return this.uda.mac.replace(/:/g, "") + ((this.uda.device_type === "UAH-Ent") ? "-" + this.uda.source_id.toUpperCase() : "");
}
// Utility function to return the fully enumerated name of this device.
get name() {
return this.controller.udaApi.getFullName(this.uda);
}
// Utility function to return the current accessory name of this device.
get accessoryName() {
return this.accessory.getService(this.hap.Service.AccessoryInformation)?.getCharacteristic(this.hap.Characteristic.Name).value ??
(this.uda?.alias ?? "Unknown");
}
// Utility function to set the current accessory name of this device.
set accessoryName(name) {
const cleanedName = validateName(name);
// Set all the internally managed names within Homebridge to the new accessory name.
this.accessory.displayName = cleanedName;
this.accessory._associatedHAPAccessory.displayName = cleanedName;
// Set all the HomeKit-visible names.
this.accessory.getService(this.hap.Service.AccessoryInformation)?.updateCharacteristic(this.hap.Characteristic.Name, cleanedName);
}
}
//# sourceMappingURL=access-device.js.map