UNPKG

homebridge-plugin-utils

Version:

Opinionated utilities to provide common capabilities and create rich configuration webUI experiences for Homebridge plugins.

256 lines 12.4 kB
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved. * * service.ts: Useful Homebridge service support functions. */ import { sanitizeName } from "./util.js"; // Cached Sets for O(1) service UUID lookups. Lazily initialized on first use. let requiresConfiguredNameUUIDs = null; let hasConfiguredNameUUIDs = null; let requiresNameUUIDs = null; let hasNameUUIDs = null; /** * Initializes the cached UUID Sets for service characteristic lookups. * * @param service - Any service instance, used to access the Service constructor and its static UUID properties. * * @internal */ function initServiceUUIDSets(service) { // Already initialized. if (requiresConfiguredNameUUIDs) { return; } // Grab the constructor from the instance of our service so we can access the static UUID properties. const ctor = service.constructor; // Services that require the ConfiguredName characteristic. requiresConfiguredNameUUIDs = new Set([ ctor.InputSource.UUID, ctor.Television.UUID, ctor.WiFiRouter.UUID ]); // Services that support the ConfiguredName characteristic (includes required). hasConfiguredNameUUIDs = new Set([ ctor.AccessoryInformation.UUID, ctor.ContactSensor.UUID, ctor.InputSource.UUID, ctor.Lightbulb.UUID, ctor.MotionSensor.UUID, ctor.OccupancySensor.UUID, ctor.SmartSpeaker.UUID, ctor.Switch.UUID, ctor.Television.UUID, ctor.Valve.UUID, ctor.WiFiRouter.UUID ]); // Services that require the Name characteristic. requiresNameUUIDs = new Set([ ctor.AccessoryInformation.UUID, ctor.Assistant.UUID, ctor.InputSource.UUID ]); // Services that support the Name characteristic (includes required). hasNameUUIDs = new Set([ ctor.AccessoryInformation.UUID, ctor.AirPurifier.UUID, ctor.AirQualitySensor.UUID, ctor.Assistant.UUID, ctor.Battery.UUID, ctor.CarbonDioxideSensor.UUID, ctor.CarbonMonoxideSensor.UUID, ctor.ContactSensor.UUID, ctor.Door.UUID, ctor.Doorbell.UUID, ctor.Fan.UUID, ctor.Fanv2.UUID, ctor.Faucet.UUID, ctor.FilterMaintenance.UUID, ctor.GarageDoorOpener.UUID, ctor.HeaterCooler.UUID, ctor.HumidifierDehumidifier.UUID, ctor.HumiditySensor.UUID, ctor.InputSource.UUID, ctor.IrrigationSystem.UUID, ctor.LeakSensor.UUID, ctor.Lightbulb.UUID, ctor.LightSensor.UUID, ctor.LockMechanism.UUID, ctor.MotionSensor.UUID, ctor.OccupancySensor.UUID, ctor.Outlet.UUID, ctor.SecuritySystem.UUID, ctor.Slats.UUID, ctor.SmartSpeaker.UUID, ctor.SmokeSensor.UUID, ctor.StatefulProgrammableSwitch.UUID, ctor.StatelessProgrammableSwitch.UUID, ctor.Switch.UUID, ctor.TargetControl.UUID, ctor.Television.UUID, ctor.TemperatureSensor.UUID, ctor.Thermostat.UUID, ctor.Valve.UUID, ctor.Window.UUID, ctor.WindowCovering.UUID ]); } /** * Utility method that either creates a new service on an accessory if needed, or returns an existing one. Optionally, it executes a callback to initialize a new * service instance. Additionally, the various name characteristics of the service are set to the specified name, and optionally added if necessary. * * @param accessory - The Homebridge accessory to check or modify. * @param serviceType - The type of service to instantiate or retrieve. * @param name - Name to be displayed to the end user for this service. * @param subtype - Optional service subtype to uniquely identify the service. * @param onServiceCreate - Optional callback invoked only when a new service is created, receiving the new service as its argument. * * @returns Returns the created or retrieved service, or `null` if service creation failed. * * @remarks * This method ensures that the service's display name and available name characteristics are updated to the specified name. If `onServiceCreate` is provided, * it will only be called for newly created services, not for existing ones. * * The `ConfiguredName` and `Name` characteristics are conditionally added or updated based on the type of service, in accordance with HomeKit requirements. * * @example * ```typescript * // Example: Ensure a Lightbulb service exists with a user-friendly name, and initialize it if newly created. * const lightbulbService = acquireService(accessory, hap.Service.Lightbulb, "Living Room Lamp", undefined, (svc: Service): void => { * * // Called only if the service is newly created. * svc.setCharacteristic(hap.Characteristic.On, false); * }); * * if(lightbulbService) { * * // Service is now available, with display name set and optional characteristics managed. * lightbulbService.updateCharacteristic(hap.Characteristic.Brightness, 75); * } * ``` * * @see setServiceName — updates the newly created (or existing) service’s name-related characteristics. * @see validService — validate or prune services after acquisition. * @category Accessory */ export function acquireService(accessory, serviceType, name, subtype, onServiceCreate) { // Ensure we have HomeKit approved naming. name = sanitizeName(name); // Find the service, if it exists. let service = subtype ? accessory.getServiceById(serviceType, subtype) : accessory.getService(serviceType); // Add the service to the accessory, if needed. if (!service) { service = new serviceType(name, subtype); // Grab the Characteristic constructor from the instance of our service so we can set the individual characteristics without needing the HAP object directly. const characteristic = service.characteristics[0].constructor; // Add the Configured Name characteristic if we don't already have it and it's available to us. if (!serviceRequiresConfiguredName(service) && serviceHasConfiguredName(service) && !service.optionalCharacteristics.some(x => (x.UUID === characteristic.ConfiguredName.UUID))) { service.addOptionalCharacteristic(characteristic.ConfiguredName); } // Add the Name characteristic if we don't already have it and it's available to us. if (!serviceRequiresName(service) && serviceHasName(service) && !service.optionalCharacteristics.some(x => (x.UUID === characteristic.Name.UUID))) { service.addOptionalCharacteristic(characteristic.Name); } // Set our name. setServiceName(service, name); accessory.addService(service); if (onServiceCreate) { onServiceCreate(service); } } return service; } /** * Validates whether a specific service should exist on the given accessory, removing the service if it fails validation. * * @param accessory - The Homebridge accessory to inspect and potentially modify. * @param serviceType - The type of Homebridge service being checked or instantiated. * @param validate - A boolean or a function that determines if the service should exist. If a function is provided, it receives a boolean indicating whether the * service currently exists, and should return `true` to keep the service, or `false` to remove it. * @param subtype - Optional service subtype to uniquely identify the service. * * @returns `true` if the service is valid (and kept), or `false` if it was removed. * * @remarks * The `validate` parameter can be either: * - a boolean (where `true` means keep the service, `false` means remove it). * - a function (which is called with `hasService: boolean` and returns whether to keep the service). * * If the service should not exist according to `validate`, and it is currently present, this function will remove it from the accessory. * * @example * ```typescript * // Remove a service if it exists * validService(accessory, Service.Switch, false); * * // Only keep a service if a configuration flag is true * validService(accessory, Service.Switch, config.enableSwitch); * * // Keep a service if it currently exists, or add it if a certain condition is met * validService(accessory, Service.Switch, (hasService) => hasService || config.enableSwitch); * ``` * * @see acquireService — to add or retrieve services. * @category Accessory */ export function validService(accessory, serviceType, validate, subtype) { // Find the service, if it exists. const service = subtype ? accessory.getServiceById(serviceType, subtype) : accessory.getService(serviceType); // Validate whether we should have the service. If not, remove it. if (!((typeof validate === "function") ? validate(!!service) : validate)) { if (service) { accessory.removeService(service); } return false; } // We have a valid service. return true; } /** * Determines whether the specified service type requires the ConfiguredName characteristic. * * @param service - The service instance to check. * @returns `true` if the service type requires the ConfiguredName characteristic. * * @internal */ function serviceRequiresConfiguredName(service) { initServiceUUIDSets(service); return requiresConfiguredNameUUIDs?.has(service.UUID) ?? false; } /** * Determines whether the specified service type supports the ConfiguredName characteristic. * * @param service - The service instance to check. * @returns `true` if the service type needs the ConfiguredName characteristic maintained. * * @internal */ function serviceHasConfiguredName(service) { initServiceUUIDSets(service); return hasConfiguredNameUUIDs?.has(service.UUID) ?? false; } /** * Determines whether the specified service type requires the Name characteristic. * * @param service - The service instance to check. * @returns `true` if the service type requires the Name characteristic. * * @internal */ function serviceRequiresName(service) { initServiceUUIDSets(service); return requiresNameUUIDs?.has(service.UUID) ?? false; } /** * Determines whether the specified service type supports the Name characteristic. * * @param service - The service instance to check. * @returns `true` if the service type needs the Name characteristic maintained. * * @internal */ function serviceHasName(service) { initServiceUUIDSets(service); return hasNameUUIDs?.has(service.UUID) ?? false; } /** * Retrieves the primary name of a service, preferring the ConfiguredName characteristic over the Name characteristic. * * @param service - The service from which to retrieve the name. * @returns The configured or display name of the service, or `undefined` if neither is set. * * @see setServiceName — to update the current name n a service. * @category Accessory */ export function getServiceName(service) { // No service, we're done. if (!service) { return undefined; } // Grab the Characteristic constructor from the instance of our service so we can set the individual characteristics without needing the HAP object directly. const characteristic = service.characteristics[0].constructor; return (service.getCharacteristic(characteristic.ConfiguredName).value ?? service.getCharacteristic(characteristic.Name).value ?? undefined); } /** * Updates the displayName and applicable name characteristics of a service to the specified value. * * @param service - The service to update. * @param name - The new name to apply to the service. * * @remarks * This function ensures the name is validated, updates the service's `displayName`, and sets the `ConfiguredName` and `Name` * characteristics when supported by the service type. * * @see acquireService — to add or retrieve services. * @see getServiceName — to retrieve the current name set on a service. * @category Accessory */ export function setServiceName(service, name) { // Grab the Characteristic constructor from the instance of our service so we can set the individual characteristics without needing the HAP object directly. const characteristic = service.characteristics[0].constructor; // Ensure we have HomeKit approved naming. name = sanitizeName(name); // Update our name. service.displayName = name; if (serviceHasConfiguredName(service)) { service.updateCharacteristic(characteristic.ConfiguredName, name); } if (serviceHasName(service)) { service.updateCharacteristic(characteristic.Name, name); } } //# sourceMappingURL=service.js.map