UNPKG

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
/* 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