homebridge-lib
Version:
Library for homebridge plugins
341 lines (312 loc) • 12.5 kB
JavaScript
// homebridge-lib/lib/AccessoryDelegate.js
//
// Library for Homebridge plugins.
// Copyright © 2017-2025 Erik Baauw. All rights reserved.
import { CharacteristicDelegate } from 'homebridge-lib/CharacteristicDelegate'
import { Delegate } from 'homebridge-lib/Delegate'
import { PropertyDelegate } from 'homebridge-lib/PropertyDelegate'
import { ServiceDelegate } from 'homebridge-lib/ServiceDelegate'
import 'homebridge-lib/ServiceDelegate/AccessoryInformation'
const startsWithUuid = /^[0-9A-F]{8}-[0-9A-F]{4}-[1-5][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}/
/** Delegate of a HomeKit accessory.
* <br>See {@link AccessoryDelegate}.
* @name AccessoryDelegate
* @type {Class}
* @memberof module:homebridge-lib
*/
/** Delegate of a HomeKit accessory.
*
* @abstract
* @extends Delegate
*/
class AccessoryDelegate extends Delegate {
/** Create a new instance of a HomeKit accessory delegate.
*
* When the corresponding HomeKit accessory was restored from persistent
* storage, it is linked to the delegate. Otherwise a new accessory
* will be created, using the values from `params`.
* @param {!Platform} platform - Reference to the corresponding platform
* plugin instance.
* @param {!object} params - Properties of the HomeKit accessory.
* @param {!string} params.id - The unique ID of the accessory, used to
* derive the HomeKit accessory UUID.<br>
* Must be unchangeable, preferably a serial number or mac address.
* @param {!string} params.name - The accessory name.<br>
* Also used to prefix log and error messages.
* @param {?string} params.category - The accessory category.
* @param {!string} params.manufacturer - The accessory manufacturer.
* @param {!string} params.model - The accessory model.
* @param {!string} params.firmware - The accessory firmware revision.
* @param {?string} params.hardware - The accessory hardware revision.
* @param {?string} params.software - The accessory software revision.
* @param {?integer} params.logLevel - The log level for the accessory
*/
constructor (platform, params = {}) {
if (params.name == null) {
throw new SyntaxError('params.name: missing')
}
super(platform, params.name)
if (params.id == null || typeof params.id !== 'string') {
throw new TypeError('params.id: not a string')
}
if (params.id === '') {
throw new RangeError('params.id: invalid id')
}
if (params.logLevel == null) {
params.logLevel = platform.logLevel
}
// Link or create associated PlatformAccessory.
this._accessory = this._platform._getAccessory(this, params)
this._context = this._accessory.context
// Setup shortcut for property values and values of the characteristics
// of the _Accessory Information_ service.
this._values = {} // by key
this._propertyDelegates = {}
this.addPropertyDelegate({ key: 'className', value: this.constructor.name, silent: true })
this.addPropertyDelegate({ key: 'version', silent: true })
this.values.version = platform.packageJson.version
this.addPropertyDelegate({ key: 'id', value: params.id, silent: true })
this.addPropertyDelegate({ key: 'logLevel', value: params.logLevel })
this.addPropertyDelegate({ key: 'name', value: this.name, silent: true })
.on('didSet', (name) => { this.name = name })
if (typeof platform.onUiRequest === 'function') {
this.addPropertyDelegate({ key: 'uiPort', silent: true })
if (platform._ui != null) {
this.values.uiPort = platform._ui.port
}
}
// Create delegate for AccessoryInformation service.
this._serviceDelegates = {}
this._accessoryInformationDelegate = new ServiceDelegate.AccessoryInformation(this, params)
// Configure PlatformAccessory.
this._accessory.on('identify', this._identify.bind(this))
this.once('initialised', () => {
const staleServices = []
for (const service of this._accessory.services) {
const key = service.UUID +
(service.subtype == null ? '' : '.' + service.subtype)
if (this._serviceDelegates[key] == null) {
if (service.UUID !== this.Services.hap.ProtocolInformation.UUID) {
service.key = key
staleServices.push(service)
}
} else {
this._serviceDelegates[key].emit('initialised')
}
}
for (const service of staleServices) {
this.warn('remove stale service %s (%s)', service.key, service.constructor.name)
this._accessory.removeService(service)
}
const staleKeys = []
for (const key in this._context) {
if (startsWithUuid.test(key)) {
if (this._serviceDelegates[key] == null) {
staleKeys.push(key)
}
} else {
if (key !== 'context' && this._propertyDelegates[key] == null) {
staleKeys.push(key)
}
}
}
for (const key of staleKeys) {
this.warn('remove stale context %s', key)
delete this._context[key]
}
})
}
/** Destroy accessory delegate and associated HomeKit accessory.
* @params {boolean} [delegateOnly=false] - Destroy the delegate, but keep the
* associated HomeKit accessory (including context).
*/
destroy (delegateOnly = false) {
this.removeAllListeners()
for (const key in this._serviceDelegates) {
this._serviceDelegates[key].destroy(delegateOnly)
}
for (const key in this._propertyDelegates) {
this._propertyDelegates[key]._destroy(delegateOnly)
}
if (delegateOnly) {
this._accessory.removeAllListeners()
return
}
this._platform._removeAccessory(this._accessory)
}
_linkServiceDelegate (serviceDelegate) {
const key = serviceDelegate._key
// this.debug('link service %s', key)
this._serviceDelegates[key] = serviceDelegate
return serviceDelegate
}
_unlinkServiceDelegate (serviceDelegate, delegateOnly) {
const key = serviceDelegate._key
// this.debug('unlink service %s', key)
delete this._serviceDelegates[key]
if (delegateOnly) {
return
}
delete this._context[key]
}
/** Creates a new {@link PropertyDelegate} instance, for a property of the
* associated HomeKit accessory.
*
* The property value is accessed through
* {@link AccessoryDelegate#values values}.
* The delegate is returned, but can also be accessed through
* {@link AccessoryDelegate#propertyDelegate propertyDelegate()}.
* @param {!object} params - Parameters of the property delegate.
* @param {!string} params.key - The key for the property delegate.<br>
* Needs to be unique with parent delegate.
// * @param {!type} params.type - The type of the property value.
* @param {?*} params.value - The initial value of the property.<br>
* Only used when the property delegate is created for the first time.
* Otherwise, the value is restored from persistent storage.
* @param {?boolean} params.logLevel - Level for homebridge log messages
* when property was set or has been changed.
* @param {?string} params.unit - The unit of the value of the property.
* @returns {PropertyDelegate}
* @throws {TypeError} When a parameter has an invalid type.
* @throws {RangeError} When a parameter has an invalid value.
* @throws {SyntaxError} When a mandatory parameter is missing or an
* optional parameter is not applicable.
*/
addPropertyDelegate (params = {}) {
if (typeof params.key !== 'string') {
throw new TypeError(`params.key: ${params.key}: invalid key`)
}
if (params.key === '') {
throw new RangeError(`params.key: ${params.key}: invalid key`)
}
const key = params.key
if (this._values[key] !== undefined) {
throw new SyntaxError(`${key}: duplicate key`)
}
const delegate = new PropertyDelegate(this, params)
this._propertyDelegates[key] = delegate
// Create shortcut for characteristic value.
Object.defineProperty(this._values, key, {
configurable: true, // make sure we can delete it again
writeable: true,
get () { return delegate.value },
set (value) { delegate.value = value }
})
return delegate
}
removePropertyDelegate (key) {
if (this._accessoryInformationDelegate.values[key] != null) {
throw new RangeError('%s: invalid key')
}
delete this._values[key]
const delegate = this._propertyDelegates[key]
delegate._destroy()
delete this._propertyDelegates[key]
delete this.context[key]
}
/** Returns the property delegate correspondig to the property key.
* @param {!string} key - The key for the property.
* @returns {PropertyDelegate}
*/
propertyDelegate (key) {
return this._propertyDelegates[key]
}
/** Values of the HomeKit characteristics for the `AccessoryInformation` service.
*
* Contains the key of each property and of each characteristic in
* {@link ServiceDelegate.AccessoryInformation AccessoryInformation}.
* When the value is written, the value of the corresponding HomeKit
* characteristic is updated.
* @type {object}
*/
get values () {
return this._values
}
/** Enable `heartbeat` events for this accessory delegate.
* @type {boolean}
*/
get heartbeatEnabled () { return this._heartbeatEnabled }
set heartbeatEnabled (value) { this._heartbeatEnabled = !!value }
setAlive () {
this.warn('setAlive() has been deprecated, use heartbeatEnabled instead')
this._heartbeatEnabled = true
}
/** Plugin-specific context to be persisted across Homebridge restarts.
*
* After restart, this object is passed back to the plugin through the
* {@link Platform#event:accessoryRestored accessoryRestored} event.
* The plugin should store enough information to re-create the accessory
* delegate, after Homebridge has restored the accessory.
* @type {object}
*/
get context () {
return this._context.context
}
/** Current log level.
*
* The log level determines what type of messages are printed:
*
* 0. Print error and warning messages.
* 1. Print error, warning, and log messages.
* 2. Print error, warning, log, and debug messages.
* 3. Print error, warning, log, debug, and verbose debug messages.
*
* Note that debug messages (level 2 and 3) are only printed when
* Homebridge was started with the `-D` or `--debug` command line option.
*
* The log level is initialised at 2 when the accessory is newly created.
* It can be changed programmatically.
* The log level is persisted across Homebridge restarts.
* @type {!integer}
*/
get logLevel () {
return this.values.logLevel == null ? this.platform.logLevel : this.values.logLevel
}
/** Inherit `logLevel` from another accessory delegate.
* @param {AccessoryDelegate} delegate - The delegate to inherit `logLevel`
* from.
*/
inheritLogLevel (delegate) {
if (!(delegate instanceof AccessoryDelegate)) {
throw new TypeError('delegate: not an AccessoryDelegate')
}
if (delegate === this) {
throw new RangeError('delegate: cannot inherit from oneself')
}
Object.defineProperty(this, 'logLevel', {
get () { return delegate.logLevel }
})
}
/** Manage `logLevel` from characteristic delegate.
* @param {CharacteristicDelegate|PropertyDelegate} delegate - The delegate
* of the `logLevel` characteristic.
* @param {Boolean} [forPlatform=false] - Manage the Platform `logLevel` as
* well.
*/
manageLogLevel (delegate, forPlatform = false) {
if (
!(delegate instanceof CharacteristicDelegate) &&
!(delegate instanceof PropertyDelegate)
) {
throw new TypeError('delegate: not a CharacteristicDelegate or PropertyDelegate')
}
Object.defineProperty(this, 'logLevel', {
get () { return delegate.value }
})
if (forPlatform) {
try {
// TODO handle multiple delegates for platform log level
// now platform log level is linked to first delegate only
Object.defineProperty(this.platform, 'logLevel', {
get () { return delegate.value }
})
} catch (error) { }
}
}
// Called by homebridge when Identify is selected.
_identify () {
this.emit('identify')
this.vdebug('context: %j', this._context)
}
}
export { AccessoryDelegate }