UNPKG

@lucavb/homebridge-shelly-ds9

Version:

Homebridge plugin for the next generation of Shelly devices

1 lines 145 kB
{"version":3,"file":"index.cjs","names":["Perms","Categories","LogLevel","Temperature","ADDON_SENSOR_MIN_ID","Humidity","ShellyPlus1","ShellyPlus1Ul","ShellyPlus1V3","ShellyPlus1Mini","ShellyPlus1MiniV3","ShellyGen4One","ShellyGen4OneMini","ShellyGen4OneAnz","Shelly1LGen3","ShellyPlus1Pm","ShellyPlus1PmUl","ShellyPlus1PmV3","ShellyPlus1PmMini","ShellyPlus1PmMiniV3","ShellyGen4OnePm","ShellyGen4OnePmAnz","ShellyPlusPmMini","ShellyPlusPmMiniV3","ShellyPlus2Pm","ShellyPlus2PmRev1","ShellyGen32Pm","ShellyGen42Pm","ShellyGen42PmAnz","ShellyPlusI4","ShellyPlusI4V3","ShellyPlusI4Dc","ShellyPlusPlugUs","ShellyPlusPlugEu","ShellyPlusPlugIt","ShellyPlusPlugUk","ShellyPlugSG3Eu","ShellyPlugAzG3Eu","ShellyOutdoorPlugSG3Eu","ShellyPlugMG3Eu","ShellyPlugPmG3Eu","ShellyPlugUsG4","ShellyPro1","ShellyPro1Rev1","ShellyPro1Rev2","ShellyPro1Rev2Ul","ShellyPro1Pm","ShellyPro1PmRev1","ShellyPro1PmRev2","ShellyPro1PmRev2Ul","ShellyPro2","ShellyPro2Rev1","ShellyPro2Rev2","ShellyPro3","ShellyPro2Pm","ShellyPro2PmRev1","ShellyPro2PmRev2","ShellyPro4Pm","ShellyPro4PmV2","ShellyPro4PmV3","ShellyProDimmer1Pm","ShellyProDimmer1Pm2","ShellyDimmer","ShellyProDimmer2Pm","ShellyProDualCoverPm","ShellyPlusPMDimmer","ShellyPlusDimmer","ShellyGen4Mini","ShellyPowerStrip4G4","ShellyPowerStrip4G4Black","DeviceDiscoverer","Shellies","MdnsDeviceDiscoverer"],"sources":["../src/utils/characteristics.ts","../src/utils/services.ts","../src/utils/device-cache.ts","../src/abilities/base.ts","../src/abilities/accessory-information.ts","../src/abilities/cover.ts","../src/abilities/light.ts","../src/abilities/outlet.ts","../src/abilities/power-meter.ts","../src/abilities/readonly-switch.ts","../src/abilities/service-label.ts","../src/abilities/stateless-programmable-switch.ts","../src/abilities/switch.ts","../src/abilities/pm1.ts","../src/abilities/temperature-sensor.ts","../src/abilities/humidity-sensor.ts","../src/accessory.ts","../src/utils/device-logger.ts","../src/device-delegates/base.ts","../src/device-delegates/shelly-plus-1.ts","../src/device-delegates/shelly-plus-1-pm.ts","../src/device-delegates/shelly-plus-pm.ts","../src/device-delegates/shelly-plus-2-pm.ts","../src/device-delegates/shelly-plus-i4.ts","../src/device-delegates/shelly-plus-plug.ts","../src/device-delegates/shelly-pro-1.ts","../src/device-delegates/shelly-pro-1-pm.ts","../src/device-delegates/shelly-pro-2.ts","../src/device-delegates/shelly-pro-3.ts","../src/device-delegates/shelly-pro-2-pm.ts","../src/device-delegates/shelly-pro-4-pm.ts","../src/device-delegates/shelly-pro-dimmer-1-pm.ts","../src/device-delegates/shelly-pro-dimmer-2-pm.ts","../src/device-delegates/shelly-pro-dual-cover-pm.ts","../src/device-delegates/shelly-plus-dimmer-pm.ts","../src/device-delegates/shelly-plus-dimmer.ts","../src/device-delegates/shelly-gen4-mini.ts","../src/device-delegates/shelly-gen4-power-strip-4.ts","../src/config.ts","../src/platform.ts","../src/index.ts"],"sourcesContent":["import { API, Characteristic, WithUUID } from 'homebridge';\n\ntype C = WithUUID<new () => Characteristic> & WithUUID<typeof Characteristic>;\n\nexport interface CustomCharacteristics {\n CurrentConsumption: C;\n ElectricCurrent: C;\n TotalConsumption: C;\n Voltage: C;\n}\n\n/**\n * Returns a set of custom HomeKit characteristics.\n * @param api - A reference to the homebridge API.\n */\nexport const createCharacteristics = (api: API): CustomCharacteristics => {\n /**\n * Current energy consumption, in watts.\n */\n class CurrentConsumption extends api.hap.Characteristic {\n static readonly UUID = 'E863F10D-079E-48FF-8F27-9C2605A29F52';\n\n constructor() {\n super('Current Consumption', CurrentConsumption.UUID, {\n format: api.hap.Formats.FLOAT,\n perms: [api.hap.Perms.NOTIFY, api.hap.Perms.PAIRED_READ],\n unit: 'W',\n minValue: 0,\n maxValue: 12000,\n minStep: 0.1,\n });\n }\n }\n\n /**\n * Current measured electric current, in amperes.\n */\n class ElectricCurrent extends api.hap.Characteristic {\n static readonly UUID = 'E863F126-079E-48FF-8F27-9C2605A29F52';\n\n constructor() {\n super('Electric Current', ElectricCurrent.UUID, {\n format: api.hap.Formats.FLOAT,\n perms: [api.hap.Perms.NOTIFY, api.hap.Perms.PAIRED_READ],\n unit: 'A',\n minValue: 0,\n maxValue: 48,\n minStep: 0.1,\n });\n }\n }\n\n /**\n * Total energy consumption, in kilowatt hours.\n */\n class TotalConsumption extends api.hap.Characteristic {\n static readonly UUID = 'E863F10C-079E-48FF-8F27-9C2605A29F52';\n\n constructor() {\n super('Total Consumption', TotalConsumption.UUID, {\n format: api.hap.Formats.FLOAT,\n perms: [api.hap.Perms.NOTIFY, api.hap.Perms.PAIRED_READ],\n unit: 'kWh',\n minValue: 0,\n maxValue: 1000000,\n minStep: 0.1,\n });\n }\n }\n\n /**\n * Current measured voltage, in volts.\n */\n class Voltage extends api.hap.Characteristic {\n static readonly UUID = 'E863F10A-079E-48FF-8F27-9C2605A29F52';\n\n constructor() {\n super('Voltage', Voltage.UUID, {\n format: api.hap.Formats.FLOAT,\n perms: [api.hap.Perms.NOTIFY, api.hap.Perms.PAIRED_READ],\n unit: 'V',\n minValue: -1000,\n maxValue: 1000,\n minStep: 0.1,\n });\n }\n }\n\n return {\n CurrentConsumption,\n ElectricCurrent,\n TotalConsumption,\n Voltage,\n };\n};\n","import { API, Service, WithUUID } from 'homebridge';\n\nimport { CustomCharacteristics } from './characteristics.ts';\n\ntype S = WithUUID<typeof Service>;\n\nexport interface CustomServices {\n PowerMeter: S;\n Pm1: S;\n}\n\n/**\n * Returns a set of custom HomeKit services.\n * @param api - A reference to the homebridge API.\n * @param characteristics - Custom characteristics used with these services.\n */\nexport const createServices = (api: API, characteristics: CustomCharacteristics): CustomServices => {\n /**\n * Reports power meter readings.\n */\n class PowerMeter extends api.hap.Service {\n static readonly UUID = 'DEDBEA44-11ED-429C-BD75-9A2286AA8707';\n\n constructor(displayName?: string, subtype?: string) {\n super(displayName, PowerMeter.UUID, subtype);\n\n this.addCharacteristic(characteristics.CurrentConsumption);\n\n this.addOptionalCharacteristic(characteristics.TotalConsumption);\n this.addOptionalCharacteristic(characteristics.ElectricCurrent);\n this.addOptionalCharacteristic(characteristics.Voltage);\n }\n }\n /**\n * Reports Pm1 readings.\n */\n class Pm1 extends api.hap.Service {\n static readonly UUID = 'DEDBEA44-11ED-429C-BD75-9A2286AA8707';\n\n constructor(displayName?: string, subtype?: string) {\n super(displayName, Pm1.UUID, subtype);\n\n this.addCharacteristic(characteristics.CurrentConsumption);\n this.addOptionalCharacteristic(characteristics.TotalConsumption);\n this.addOptionalCharacteristic(characteristics.ElectricCurrent);\n this.addOptionalCharacteristic(characteristics.Voltage);\n }\n }\n\n return {\n PowerMeter,\n Pm1,\n };\n};\n","import { Logger } from 'homebridge';\nimport { promises as fs } from 'fs';\nimport { resolve } from 'path';\n\nimport { Device, DeviceId, WebSocketRpcHandler } from '@lucavb/shellies-ds9';\n\nconst FILENAME = '.shelly-ds9.json';\nconst LEGACY_FILENAME = '.shelly-ng.json';\n\nconst SAVE_DELAY = 1000;\n\nexport interface CachedDeviceInfo {\n /**\n * Device ID.\n */\n id: DeviceId;\n /**\n * Device model.\n */\n model: string;\n /**\n * RPC handler protocol.\n */\n protocol: string;\n /**\n * The device's IP address or hostname.\n */\n hostname?: string;\n}\n\nexport interface DeviceStorage {\n devices: CachedDeviceInfo[];\n}\n\n/**\n * Handles saving and loading device information to a cache file.\n */\nexport class DeviceCache {\n /**\n * A path to the cache file.\n */\n readonly path: string;\n\n /**\n * A path to the legacy cache file from the upstream plugin.\n */\n readonly legacyPath: string;\n\n /**\n * Holds all devices loaded from the cache file.\n */\n protected devices = new Map<DeviceId, CachedDeviceInfo>();\n\n private saveTimeout: ReturnType<typeof setTimeout> | null = null;\n\n /**\n * @param storagePath - A path to the directory that the devices will be stored in.\n * @param log - A logging device.\n */\n constructor(\n storagePath: string,\n readonly log: Logger,\n ) {\n this.path = resolve(storagePath, FILENAME);\n this.legacyPath = resolve(storagePath, LEGACY_FILENAME);\n }\n\n /**\n * Loads cached devices.\n */\n async load() {\n this.devices.clear();\n\n let data: string | undefined;\n let loadedFromLegacy = false;\n\n for (const filePath of [this.path, this.legacyPath]) {\n try {\n await fs.access(filePath);\n data = await fs.readFile(filePath, { encoding: 'utf8' });\n loadedFromLegacy = filePath === this.legacyPath;\n break;\n } catch {\n // try next path\n }\n }\n\n if (data === undefined) {\n this.log.debug('Device cache file ' + this.path + ' not found');\n return;\n }\n\n const s = JSON.parse(data) as DeviceStorage;\n\n this.log.debug(`Loaded ${s.devices.length} device(s) from cache`);\n\n for (const d of s.devices) {\n this.devices.set(d.id, d);\n }\n\n if (loadedFromLegacy) {\n this.saveDelayed();\n }\n }\n\n /**\n * Saves the devices to cache.\n */\n async save() {\n // serialize the devices\n const s = { devices: Array.from(this.devices.values()) };\n const data = JSON.stringify(s);\n\n this.log.debug(`Saving ${s.devices.length} device(s) to cache`);\n\n // write them to disk\n return fs.writeFile(this.path, data);\n }\n\n /**\n * Saves the devices to cache after a short delay.\n * Multiple calls to this method within the delay will be debounced.\n */\n saveDelayed() {\n // clear any previously set timeout\n if (this.saveTimeout !== null) {\n clearTimeout(this.saveTimeout);\n }\n\n this.saveTimeout = setTimeout(async () => {\n this.saveTimeout = null;\n\n try {\n await this.save();\n } catch (e) {\n this.log.error('Failed to save devices to cache:', e instanceof Error ? e.message : e);\n }\n }, SAVE_DELAY);\n }\n\n /**\n * Returns device info for the given device ID.\n */\n get(id: DeviceId): CachedDeviceInfo | undefined {\n return this.devices.get(id);\n }\n\n /**\n * Stores the given device info.\n * @param d - The device info.\n * @param autoSave - Whether `saveDelayed()` should be automatically invoked.\n */\n set(d: CachedDeviceInfo, autoSave = true) {\n this.devices.set(d.id, d);\n\n if (autoSave === true) {\n this.saveDelayed();\n }\n }\n\n /**\n * Stores info about the given device in cache.\n * @param device - The device.\n * @param autoSave - Whether `saveDelayed()` should be automatically invoked.\n */\n storeDevice(device: Device, autoSave = true) {\n const protocol = device.rpcHandler.protocol;\n let hostname;\n\n if (protocol === 'websocket') {\n hostname = (device.rpcHandler as WebSocketRpcHandler).hostname;\n }\n\n this.set({ hostname, id: device.id, model: device.model, protocol }, autoSave);\n }\n\n /**\n * Deletes info about the device with the given ID from cache.\n * @param id - The device ID.\n * @param autoSave - Whether `saveDelayed()` should be automatically invoked.\n */\n delete(id: DeviceId, autoSave = true) {\n this.devices.delete(id);\n\n if (autoSave === true) {\n this.saveDelayed();\n }\n }\n\n /**\n * Returns a new Iterator object that contains each device.\n */\n [Symbol.iterator](): IterableIterator<CachedDeviceInfo> {\n return this.devices.values();\n }\n}\n","import { API, Characteristic, PlatformAccessory, Service, WithUUID } from 'homebridge';\n\nimport { CustomCharacteristics } from '../utils/characteristics.ts';\nimport { CustomServices } from '../utils/services.ts';\nimport { DeviceLogger } from '../utils/device-logger.ts';\nimport { ShellyPlatform } from '../platform.ts';\n\nexport type ServiceClass = WithUUID<typeof Service>;\n\n/**\n * Base class for all abilities.\n * An ability is roughly equivalent to a HomeKit service.\n */\nexport abstract class Ability {\n private _platformAccessory: PlatformAccessory | null = null;\n\n /**\n * The associated platform accessory.\n */\n get platformAccessory(): PlatformAccessory {\n if (this._platformAccessory === null) {\n throw new Error('Ability has not yet been setup');\n }\n return this._platformAccessory;\n }\n\n private _platform: ShellyPlatform | null = null;\n\n /**\n * A reference to the platform.\n */\n protected get platform(): ShellyPlatform {\n if (this._platform === null) {\n throw new Error('Ability has not yet been setup');\n }\n return this._platform;\n }\n\n /**\n * A reference to the homebridge API.\n */\n protected get api(): API {\n return this.platform.api;\n }\n\n /**\n * Shorthand property.\n */\n protected get Characteristic(): typeof Characteristic {\n return this.platform.api.hap.Characteristic;\n }\n\n /**\n * Shorthand property.\n */\n protected get Service(): typeof Service {\n return this.platform.api.hap.Service;\n }\n\n /**\n * Shorthand property.\n */\n protected get customCharacteristics(): CustomCharacteristics {\n return this.platform.customCharacteristics;\n }\n\n /**\n * Shorthand property.\n */\n protected get customServices(): CustomServices {\n return this.platform.customServices;\n }\n\n private _log: DeviceLogger | null = null;\n\n /**\n * The logging device to use.\n */\n protected get log(): DeviceLogger {\n if (this._log === null) {\n throw new Error('Ability has not yet been setup');\n }\n return this._log;\n }\n\n private _service: Service | null = null;\n\n /**\n * The HomeKit service that this ability uses.\n */\n protected get service(): Service {\n if (this._service === null) {\n throw new Error('Ability has not yet been setup');\n }\n return this._service;\n }\n\n private _active = true;\n\n /**\n * Whether this ability is active.\n * Setting an ability to inactive will remove its HomeKit service.\n */\n get active(): boolean {\n return this._active;\n }\n\n set active(value) {\n if (value === this._active) {\n return;\n }\n\n this._active = value;\n this.update();\n }\n\n /**\n * @param serviceName - A name of the service.\n * @param serviceSubtype - A unique identifier for the service.\n */\n constructor(\n protected readonly serviceName?: string,\n protected readonly serviceSubtype?: string,\n ) {}\n\n /**\n * Sets up this ability.\n * This method is called by the parent accessory every time it becomes active.\n * @param platformAccessory - The homebridge platform accessory to use.\n * @param platform - A reference to the platform.\n * @param log - The logger to use.\n */\n setup(platformAccessory: PlatformAccessory, platform: ShellyPlatform, log: DeviceLogger) {\n this._platformAccessory = platformAccessory;\n this._platform = platform;\n this._log = log;\n\n this.update();\n }\n\n /**\n * Sets `active` to the given value.\n * This method can be used when chaining calls, as it returns a reference to `this`.\n * @param value - Whether the ability should be active.\n */\n setActive(value: boolean): this {\n this.active = value;\n return this;\n }\n\n /**\n * Determines whether this ability is active.\n * The default implementation simply returns the value of the `active` property.\n * Subclasses can override this method to add more conditions.\n */\n protected isActive(): boolean {\n return this.active;\n }\n\n /**\n * Updates this ability based on whether it is active.\n * If active, its service will be added and initialized.\n * If inactive, its service will be removed.\n */\n protected update() {\n if (this._platformAccessory === null) {\n // abort if setup() hasn't been called yet\n return;\n }\n\n if (this.isActive()) {\n // we're active, add and initialize our service\n if (this._service === null) {\n this._service = this.addService();\n this.initialize();\n }\n } else {\n // we're inactive, detach from and remove our service\n if (this._service !== null) {\n this.detach();\n }\n\n this.removeService();\n this._service = null;\n }\n }\n\n /**\n * Returns a service for this ability.\n * If the platform accessory has a matching service, it will be returned. Otherwise, the service will be added.\n */\n protected addService(): Service | null {\n let service: Service | undefined;\n if (this.serviceName && this.serviceSubtype) {\n service =\n this.platformAccessory.getService(this.serviceName) ||\n this.platformAccessory.addService(this.serviceClass, this.serviceName, this.serviceSubtype);\n } else {\n service = this.platformAccessory.getService(this.serviceClass);\n }\n return service ?? null;\n }\n\n /**\n * Removes this ability's service from the platform accessory.\n */\n protected removeService() {\n let service: Service | undefined;\n\n // since the platform accessory may have been loaded from cache with a service created previously,\n // we can't just rely on the _service property here\n if (this._service !== null) {\n service = this._service;\n } else if (this.serviceName && this.serviceSubtype) {\n service = this.platformAccessory.getService(this.serviceName);\n } else {\n service = this.platformAccessory.getService(this.serviceClass);\n }\n\n if (service) {\n this.platformAccessory.removeService(service);\n }\n }\n\n /**\n * Helper method that removes a characteristic based on its class (Service.removeCharacteristic()\n * only accepts an instance).\n * @param characteristic - The characteristic to remove.\n */\n protected removeCharacteristic(\n characteristic: WithUUID<new () => Characteristic> & WithUUID<typeof Characteristic>,\n ) {\n const s = this.service;\n\n // getCharacteristic() will add the characteristic if it doesn't exist, so we need to use testCharacteristic() to avoid\n // adding and then immediately removing it\n if (s.testCharacteristic(characteristic)) {\n s.removeCharacteristic(s.getCharacteristic(characteristic));\n }\n }\n\n /**\n * Subclasses should implement this method to return the HomeKit service type to use.\n */\n protected abstract get serviceClass(): ServiceClass;\n\n /**\n * Subclasses should use this method to initialize the service and attach their event listeners.\n */\n protected abstract initialize();\n\n /**\n * Subclasses should use this method to remove their event listeners.\n */\n abstract detach();\n\n /**\n * Removes all event listeners and all references to the platform accessory.\n * This method is called by the parent accessory every time it becomes inactive.\n * Note that this method doesn't remove the service from the platform accessory as it is assumed that\n * the entire platform accessory is about to be unregistered and discarded.\n */\n destroy() {\n this.detach();\n\n this._platformAccessory = null;\n this._platform = null;\n this._log = null;\n this._service = null;\n }\n}\n","import { Device } from '@lucavb/shellies-ds9';\n\nimport { Ability, ServiceClass } from './base.ts';\n\n/**\n * Handles the AccessoryInformation service.\n */\nexport class AccessoryInformationAbility extends Ability {\n /**\n * @param device - The associated device.\n */\n constructor(readonly device: Device) {\n super();\n }\n\n protected get serviceClass(): ServiceClass {\n return this.Service.AccessoryInformation;\n }\n\n protected initialize() {\n this.service\n .setCharacteristic(this.Characteristic.Name, this.platformAccessory.displayName)\n .setCharacteristic(this.Characteristic.Manufacturer, 'Allterco')\n .setCharacteristic(this.Characteristic.Model, this.device.modelName)\n .setCharacteristic(this.Characteristic.SerialNumber, this.device.macAddress)\n .setCharacteristic(this.Characteristic.FirmwareRevision, this.device.firmware.version || '1.0.0');\n }\n\n detach() {\n // no event handlers\n }\n}\n","import { CharacteristicValue } from 'homebridge';\nimport { Cover } from '@lucavb/shellies-ds9';\n\nimport { Ability, ServiceClass } from './base.ts';\n\nconst names = {\n door: 'Door',\n window: 'Window',\n windowCovering: 'Window Covering',\n};\n\nexport class CoverAbility extends Ability {\n /**\n * @param component - The cover component to control.\n * @param type - The type of cover.\n */\n constructor(\n readonly component: Cover,\n readonly type: 'door' | 'window' | 'windowCovering' = 'window',\n ) {\n super(`${names[type]} ${component.id + 1}`, `${type}-${component.id}`);\n }\n\n protected get serviceClass(): ServiceClass {\n if (this.type === 'door') {\n return this.Service.Door;\n } else if (this.type === 'windowCovering') {\n return this.Service.WindowCovering;\n }\n return this.Service.Window;\n }\n\n /**\n * The current state of the cover.\n */\n protected get positionState(): CharacteristicValue {\n const state = this.component.state;\n\n if (state === 'opening') {\n return this.Characteristic.PositionState.INCREASING;\n } else if (state === 'closing') {\n return this.Characteristic.PositionState.DECREASING;\n }\n\n return this.Characteristic.PositionState.STOPPED;\n }\n\n /**\n * The current position of the cover.\n */\n protected get currentPosition(): number {\n return this.component.current_pos ?? 0;\n }\n\n /**\n * The target position that the cover is moving towards.\n */\n protected get targetPosition(): number {\n return this.component.target_pos ?? this.currentPosition;\n }\n\n protected initialize() {\n // abort if this cover hasn't been calibrated\n if (!this.component.pos_control) {\n this.log.warn('Only calibrated covers are supported.');\n return;\n }\n\n // set the initial values\n this.service\n .setCharacteristic(this.Characteristic.PositionState, this.positionState)\n .setCharacteristic(this.Characteristic.CurrentPosition, this.currentPosition)\n .setCharacteristic(this.Characteristic.TargetPosition, this.targetPosition);\n\n // listen for commands from HomeKit\n this.service\n .getCharacteristic(this.Characteristic.TargetPosition)\n .onSet(this.targetPositionSetHandler.bind(this));\n\n // listen for updates from the device\n this.component\n .on('change:state', this.stateChangeHandler, this)\n .on('change:current_pos', this.currentPosChangeHandler, this)\n .on('change:target_pos', this.targetPosChangeHandler, this);\n }\n\n detach() {\n this.component\n .off('change:state', this.stateChangeHandler, this)\n .off('change:current_pos', this.currentPosChangeHandler, this)\n .off('change:target_pos', this.targetPosChangeHandler, this);\n }\n\n /**\n * Handles changes to the TargetPosition characteristic.\n */\n protected async targetPositionSetHandler(value: CharacteristicValue) {\n if (value === this.component.target_pos) {\n return;\n }\n\n try {\n await this.component.goToPosition(value as number);\n } catch (e) {\n this.log.error('Failed to set target position:', e instanceof Error ? e.message : e);\n throw new this.api.hap.HapStatusError(this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);\n }\n }\n\n /**\n * Handles changes to the `state` property.\n */\n protected stateChangeHandler() {\n this.log.debug(`${this.component.id} state changed to ${this.positionState}`, {\n target: this.targetPosition,\n current: this.currentPosition,\n });\n this.updateStates();\n }\n\n /**\n * Updates all states.\n *\n * Shelly does not send all attributes in a single notification.\n * We apparently need to update all states when any of them change, otherwise HomeKit\n * gets confused and thinks the cover is in a different state than it actually is.\n */\n protected updateStates() {\n this.service.getCharacteristic(this.Characteristic.PositionState).updateValue(this.positionState);\n this.service.getCharacteristic(this.Characteristic.TargetPosition).updateValue(this.targetPosition);\n this.service.getCharacteristic(this.Characteristic.CurrentPosition).updateValue(this.currentPosition);\n }\n\n /**\n * Handles changes to the `current_pos` property.\n */\n protected currentPosChangeHandler() {\n this.log.debug(`${this.component.id} position changed to ${this.currentPosition}`, {\n target: this.targetPosition,\n state: this.positionState,\n });\n this.updateStates();\n\n // Shelly does not update the target position when it is triggered with a physical switch.\n // If we don't change the target position, HomeKit waits for the original position forever.\n this.service.getCharacteristic(this.Characteristic.TargetPosition).updateValue(this.currentPosition);\n }\n\n /**\n * Handles changes to the `target_pos` property.\n */\n protected targetPosChangeHandler() {\n this.log.debug(`${this.component.id} target position changed to ${this.targetPosition}`, {\n state: this.positionState,\n current: this.currentPosition,\n });\n this.updateStates();\n }\n}\n","import { CharacteristicValue } from 'homebridge';\nimport { CharacteristicValue as ShelliesCharacteristicValue, Light } from '@lucavb/shellies-ds9';\n\nimport { Ability, ServiceClass } from './base.ts';\n\nexport class LightAbility extends Ability {\n /**\n * @param component - The light component to control.\n */\n constructor(readonly component: Light) {\n super(`Light ${component.id + 1}`, `light-${component.id}`);\n }\n\n protected get serviceClass(): ServiceClass {\n return this.Service.Lightbulb;\n }\n\n protected initialize() {\n // set the initial value\n this.service.setCharacteristic(this.Characteristic.On, this.component.output);\n\n // listen for commands from HomeKit\n this.service.getCharacteristic(this.Characteristic.On).onSet(this.onSetHandler.bind(this));\n this.service.getCharacteristic(this.Characteristic.Brightness).onSet(this.brightnessSetHandler.bind(this));\n\n // listen for updates from the device\n this.component.on('change:output', this.outputChangeHandler, this);\n this.component.on('change:brightness', this.brightnessChangeHandler, this);\n }\n\n detach() {\n this.component.off('change:output', this.outputChangeHandler, this);\n this.component.off('change:brightness', this.brightnessChangeHandler, this);\n }\n\n /**\n * Handles changes to the Light.On characteristic.\n */\n protected async onSetHandler(value: CharacteristicValue) {\n if (value === this.component.output) {\n return;\n }\n\n try {\n await this.component.set(value as boolean);\n } catch (e) {\n this.log.error('Failed to set light:', e instanceof Error ? e.message : e);\n throw new this.api.hap.HapStatusError(this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);\n }\n }\n\n /**\n * Handles changes to the `output` property.\n */\n protected outputChangeHandler(value: ShelliesCharacteristicValue) {\n if (value) {\n this.log.info('Light Status(' + this.component.id + '): on');\n } else {\n this.log.info('Light Status(' + this.component.id + '): off');\n }\n this.service.getCharacteristic(this.Characteristic.On).updateValue(value as boolean);\n }\n\n /**\n * Handles changes to the Light.Brightness characteristic.\n */\n protected async brightnessSetHandler(value: CharacteristicValue) {\n if (value === this.component.brightness) {\n return;\n }\n\n try {\n await this.component.set(undefined, value as number);\n } catch (e) {\n this.log.error('Failed to set light:', e instanceof Error ? e.message : e);\n throw new this.api.hap.HapStatusError(this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);\n }\n }\n\n /**\n * Handles changes to the `brightness` property.\n */\n protected brightnessChangeHandler(value: ShelliesCharacteristicValue) {\n this.log.info('Light Status(' + this.component.id + '): ' + value);\n this.service.getCharacteristic(this.Characteristic.Brightness).updateValue(value as number);\n }\n}\n","import { CharacteristicValue } from 'homebridge';\nimport { CharacteristicValue as ShelliesCharacteristicValue, Switch } from '@lucavb/shellies-ds9';\n\nimport { Ability, ServiceClass } from './base.ts';\n\nexport class OutletAbility extends Ability {\n /**\n * @param component - The switch component to control.\n */\n constructor(readonly component: Switch) {\n super(`Outlet ${component.id + 1}`, `outlet-${component.id}`);\n }\n\n protected get serviceClass(): ServiceClass {\n return this.Service.Outlet;\n }\n\n protected initialize() {\n // set the initial values\n this.service\n .setCharacteristic(this.Characteristic.On, this.component.output)\n .setCharacteristic(\n this.Characteristic.OutletInUse,\n this.component.apower !== undefined && this.component.apower !== 0,\n );\n\n // listen for commands from HomeKit\n this.service.getCharacteristic(this.Characteristic.On).onSet(this.onSetHandler.bind(this));\n\n // listen for updates from the device\n this.component\n .on('change:output', this.outputChangeHandler, this)\n .on('change:apower', this.apowerChangeHandler, this);\n }\n\n detach() {\n this.component\n .off('change:output', this.outputChangeHandler, this)\n .off('change:apower', this.apowerChangeHandler, this);\n }\n\n /**\n * Handles changes to the Outlet.On characteristic.\n */\n protected async onSetHandler(value: CharacteristicValue) {\n if (value === this.component.output) {\n return;\n }\n\n try {\n await this.component.set(value as boolean);\n } catch (e) {\n this.log.error('Failed to set switch:', e instanceof Error ? e.message : e);\n throw new this.api.hap.HapStatusError(this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);\n }\n }\n\n /**\n * Handles changes to the `output` property.\n */\n protected outputChangeHandler(value: ShelliesCharacteristicValue) {\n if (value) {\n this.log.info('Switch Status(' + this.component.id + '): on');\n } else {\n this.log.info('Switch Status(' + this.component.id + '): off');\n }\n\n this.service.getCharacteristic(this.Characteristic.On).updateValue(value as boolean);\n }\n\n /**\n * Handles changes to the `apower` property.\n */\n protected apowerChangeHandler(value: ShelliesCharacteristicValue) {\n this.service.getCharacteristic(this.Characteristic.OutletInUse).updateValue(value as number);\n }\n}\n","import {\n CharacteristicValue as ShelliesCharacteristicValue,\n Cover,\n Switch,\n SwitchEnergyCounterAttributes,\n} from '@lucavb/shellies-ds9';\n\nimport { Ability, ServiceClass } from './base.ts';\n\n/**\n * This ability sets up a custom service that reports power meter readings.\n */\nexport class PowerMeterAbility extends Ability {\n /**\n * @param component - The switch or cover component to get readings from.\n */\n constructor(readonly component: Switch | Cover) {\n super(`Power Meter ${component.id + 1}`, `power-meter-${component.id}`);\n }\n\n protected get serviceClass(): ServiceClass {\n return this.customServices.PowerMeter;\n }\n\n protected initialize() {\n const s = this.service;\n const c = this.component;\n const cc = this.customCharacteristics;\n\n // setup Current Consumption\n s.setCharacteristic(cc.CurrentConsumption, c.apower ?? 0);\n\n c.on('change:apower', this.apowerChangeHandler, this);\n\n // setup Voltage\n if (c.voltage !== undefined) {\n s.setCharacteristic(cc.Voltage, c.voltage);\n\n c.on('change:voltage', this.voltageChangeHandler, this);\n } else {\n this.removeCharacteristic(cc.Voltage);\n }\n\n // setup Electric Current\n if (c.current !== undefined) {\n s.setCharacteristic(cc.ElectricCurrent, c.current);\n\n c.on('change:current', this.currentChangeHandler, this);\n } else {\n this.removeCharacteristic(cc.ElectricCurrent);\n }\n\n // setup Total Consumption\n if (c.aenergy !== undefined) {\n s.setCharacteristic(cc.TotalConsumption, c.aenergy.total / 1000);\n\n c.on('change:aenergy', this.aenergyChangeHandler, this);\n } else {\n this.removeCharacteristic(cc.TotalConsumption);\n }\n }\n\n detach() {\n this.component\n .off('change:apower', this.apowerChangeHandler, this)\n .off('change:voltage', this.voltageChangeHandler, this)\n .off('change:current', this.currentChangeHandler, this)\n .off('change:aenergy', this.aenergyChangeHandler, this);\n }\n\n /**\n * Handles changes to the `apower` property.\n */\n protected apowerChangeHandler(value: ShelliesCharacteristicValue) {\n /**\n * Handles changes to the 'apower' property. HomeKit does not support negative values for the 'Current Consumption' characteristic.\n * For the use case of this plugin, knowing the negative voltage is not necessary, so we ensure that the value is never negative.\n */\n const positiveValue = Math.max(0, value as number);\n this.service.updateCharacteristic(this.customCharacteristics.CurrentConsumption, positiveValue);\n }\n\n /**\n * Handles changes to the `voltage` property.\n */\n protected voltageChangeHandler(value: ShelliesCharacteristicValue) {\n this.service.updateCharacteristic(this.customCharacteristics.Voltage, value as number);\n }\n\n /**\n * Handles changes to the `current` property.\n */\n protected currentChangeHandler(value: ShelliesCharacteristicValue) {\n this.service.updateCharacteristic(this.customCharacteristics.ElectricCurrent, value as number);\n }\n\n /**\n * Handles changes to the `aenergy` property.\n */\n protected aenergyChangeHandler(value: ShelliesCharacteristicValue) {\n const attr = value as unknown as SwitchEnergyCounterAttributes;\n\n this.service.updateCharacteristic(this.customCharacteristics.TotalConsumption, attr.total / 1000);\n }\n}\n","import { Perms } from 'homebridge';\nimport { CharacteristicValue as ShelliesCharacteristicValue, Input } from '@lucavb/shellies-ds9';\n\nimport { Ability, ServiceClass } from './base.ts';\n\n/**\n * This ability creates a switch that can't be controlled from HomeKit, it only reflects\n * the device's input state.\n */\nexport class ReadonlySwitchAbility extends Ability {\n /**\n * @param component - The input component to represent.\n */\n constructor(readonly component: Input) {\n super(`Switch ${component.id + 1}`, `readonly-switch-${component.id}`);\n }\n\n protected get serviceClass(): ServiceClass {\n return this.Service.Switch;\n }\n\n protected initialize() {\n this.service\n .getCharacteristic(this.Characteristic.On)\n // remove the write permissions\n .setProps({\n perms: [Perms.NOTIFY, Perms.PAIRED_READ],\n })\n // set the initial value\n .setValue(this.component.state ?? false);\n\n // listen for updates from the device\n this.component.on('change:state', this.stateChangeHandler, this);\n }\n\n detach() {\n this.component.off('change:state', this.stateChangeHandler, this);\n }\n\n /**\n * Handles changes to the `state` property.\n */\n protected stateChangeHandler(value: ShelliesCharacteristicValue) {\n const v: boolean = value === null ? false : (value as boolean);\n if (value) {\n this.log.info('Switch Status(' + this.component.id + '): on');\n } else {\n this.log.info('Switch Status(' + this.component.id + '): off');\n }\n this.service.getCharacteristic(this.Characteristic.On).updateValue(v);\n }\n}\n","import { Ability, ServiceClass } from './base.ts';\n\nexport class ServiceLabelAbility extends Ability {\n /**\n * @param namespace - The naming schema for the accessory.\n */\n constructor(readonly namespace: 'dots' | 'arabicNumerals' = 'arabicNumerals') {\n super();\n }\n\n protected get serviceClass(): ServiceClass {\n return this.Service.ServiceLabel;\n }\n\n protected initialize() {\n const SLN = this.Characteristic.ServiceLabelNamespace;\n\n // set the namespace\n this.service.setCharacteristic(SLN, this.namespace === 'dots' ? SLN.DOTS : SLN.ARABIC_NUMERALS);\n }\n\n detach() {\n // nothing to detach\n }\n}\n","import { Input } from '@lucavb/shellies-ds9';\n\nimport { Ability, ServiceClass } from './base.ts';\n\nenum ButtonPress {\n Single = 'single',\n Double = 'double',\n Long = 'long',\n}\n\nexport class StatelessProgrammableSwitchAbility extends Ability {\n /**\n * @param component - The input component to control.\n */\n constructor(readonly component: Input) {\n super(`Button ${component.id + 1}`, `stateless-programmable-switch-${component.id}`);\n }\n\n protected get serviceClass(): ServiceClass {\n return this.Service.StatelessProgrammableSwitch;\n }\n\n protected initialize() {\n // set the index number for this switch\n this.service.setCharacteristic(this.Characteristic.ServiceLabelIndex, this.component.id + 1);\n\n // listen for button press events\n this.component\n .on('singlePush', this.singlePushHandler, this)\n .on('doublePush', this.doublePushHandler, this)\n .on('longPush', this.longPushHandler, this);\n }\n\n detach() {\n this.component\n .off('singlePush', this.singlePushHandler, this)\n .off('doublePush', this.doublePushHandler, this)\n .off('longPush', this.longPushHandler, this);\n }\n\n /**\n * Triggers a button press event.\n * @param type - The type of button press to trigger.\n */\n protected triggerPress(type: ButtonPress) {\n this.log.debug(`Input ${this.component.id}: ${type} press`);\n\n const PSE = this.Characteristic.ProgrammableSwitchEvent;\n let value: number;\n\n // get the corresponding characteristic value\n switch (type) {\n case ButtonPress.Single:\n value = PSE.SINGLE_PRESS;\n break;\n\n case ButtonPress.Double:\n value = PSE.DOUBLE_PRESS;\n break;\n\n case ButtonPress.Long:\n value = PSE.LONG_PRESS;\n break;\n }\n\n // update the characteristic\n this.service.getCharacteristic(this.Characteristic.ProgrammableSwitchEvent).updateValue(value);\n }\n\n /**\n * Handles 'singlePush' events from our input component.\n */\n protected singlePushHandler() {\n this.triggerPress(ButtonPress.Single);\n }\n\n /**\n * Handles 'doublePush' events from our input component.\n */\n protected doublePushHandler() {\n this.triggerPress(ButtonPress.Double);\n }\n\n /**\n * Handles 'longPush' events from our input component.\n */\n protected longPushHandler() {\n this.triggerPress(ButtonPress.Long);\n }\n}\n","import { CharacteristicValue } from 'homebridge';\nimport { CharacteristicValue as ShelliesCharacteristicValue, Switch } from '@lucavb/shellies-ds9';\n\nimport { Ability, ServiceClass } from './base.ts';\n\nexport class SwitchAbility extends Ability {\n /**\n * @param component - The switch component to control.\n */\n constructor(readonly component: Switch) {\n super(`Switch ${component.id + 1}`, `switch-${component.id}`);\n }\n\n protected get serviceClass(): ServiceClass {\n return this.Service.Switch;\n }\n\n protected initialize() {\n // set the initial value\n this.service.setCharacteristic(this.Characteristic.On, this.component.output);\n\n // listen for commands from HomeKit\n this.service.getCharacteristic(this.Characteristic.On).onSet(this.onSetHandler.bind(this));\n\n // listen for updates from the device\n this.component.on('change:output', this.outputChangeHandler, this);\n }\n\n detach() {\n this.component.off('change:output', this.outputChangeHandler, this);\n }\n\n /**\n * Handles changes to the Switch.On characteristic.\n */\n protected async onSetHandler(value: CharacteristicValue) {\n if (value === this.component.output) {\n return;\n }\n\n try {\n await this.component.set(value as boolean);\n } catch (e) {\n this.log.error('Failed to set switch:', e instanceof Error ? e.message : e);\n throw new this.api.hap.HapStatusError(this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);\n }\n }\n\n /**\n * Handles changes to the `output` property.\n */\n protected outputChangeHandler(value: ShelliesCharacteristicValue) {\n if (value) {\n this.log.info('Switch Status(' + this.component.id + '): on');\n } else {\n this.log.info('Switch Status(' + this.component.id + '): off');\n }\n this.service.getCharacteristic(this.Characteristic.On).updateValue(value as boolean);\n }\n}\n","//import { Perms } from \"homebridge\";\nimport { CharacteristicValue as ShelliesCharacteristicValue, Pm1, Pm1AenergyStatus } from '@lucavb/shellies-ds9';\n\nimport { Ability, ServiceClass } from './base.ts';\n\n/**\n * This ability sets up a custom service that reports power meter readings.\n */\nexport class Pm1Ability extends Ability {\n /**\n * @param componentPm1 - The switch or cover component to get readings from.\n */\n constructor(readonly componentPm1: Pm1) {\n super(`Pm1 ${componentPm1.id + 1}`, `Pm1-${componentPm1.id}`);\n }\n\n protected get serviceClass(): ServiceClass {\n return this.customServices.Pm1;\n }\n\n protected initialize() {\n const s = this.service;\n const cPm1 = this.componentPm1;\n const cc = this.customCharacteristics;\n\n // setup Current Consumption\n s.setCharacteristic(cc.CurrentConsumption, cPm1.apower ?? 0);\n // set the initial value\n s.setCharacteristic(this.Characteristic.On, false);\n\n cPm1.on('change:apower', this.apowerChangeHandler, this);\n\n // setup Voltage\n if (cPm1.voltage !== undefined) {\n s.setCharacteristic(cc.Voltage, cPm1.voltage);\n\n cPm1.on('change:voltage', this.voltageChangeHandler, this);\n } else {\n this.removeCharacteristic(cc.Voltage);\n }\n\n // setup Electric Current\n if (cPm1.current !== undefined) {\n s.setCharacteristic(cc.ElectricCurrent, cPm1.current);\n\n cPm1.on('change:current', this.currentChangeHandler, this);\n } else {\n this.removeCharacteristic(cc.ElectricCurrent);\n }\n\n // setup Total Consumption\n if (cPm1.aenergy !== undefined) {\n s.setCharacteristic(cc.TotalConsumption, cPm1.aenergy.total / 1000);\n\n cPm1.on('change:aenergy', this.aenergyChangeHandler, this);\n } else {\n this.removeCharacteristic(cc.TotalConsumption);\n }\n }\n\n detach() {\n this.componentPm1\n .off('change:apower', this.apowerChangeHandler, this)\n .off('change:voltage', this.voltageChangeHandler, this)\n .off('change:current', this.currentChangeHandler, this)\n .off('change:aenergy', this.aenergyChangeHandler, this);\n }\n\n /**\n * Handles changes to the `apower` property.\n */\n protected apowerChangeHandler(value: ShelliesCharacteristicValue) {\n this.service.updateCharacteristic(this.customCharacteristics.CurrentConsumption, value as number);\n // set status\n if (typeof value === 'number') {\n //value is definitely a number and not null\n if (value >= 1) {\n //this.log.info('Switch Status('+this.component.id+'): on');\n this.service.updateCharacteristic(this.Characteristic.On, true);\n } else {\n // this.log.info('Switch Status('+this.component.id+'): off');\n this.service.updateCharacteristic(this.Characteristic.On, false);\n }\n }\n }\n\n /**\n * Handles changes to the `voltage` property.\n */\n protected voltageChangeHandler(value: ShelliesCharacteristicValue) {\n this.service.updateCharacteristic(this.customCharacteristics.Voltage, value as number);\n }\n\n /**\n * Handles changes to the `current` property.\n */\n protected currentChangeHandler(value: ShelliesCharacteristicValue) {\n this.service.updateCharacteristic(this.customCharacteristics.ElectricCurrent, value as number);\n }\n\n /**\n * Handles changes to the `aenergy` property.\n */\n protected aenergyChangeHandler(value: ShelliesCharacteristicValue) {\n const attr = value as unknown as Pm1AenergyStatus;\n\n this.service.updateCharacteristic(this.customCharacteristics.TotalConsumption, attr.total / 1000);\n }\n}\n","import { CharacteristicValue as ShelliesCharacteristicValue, Temperature } from '@lucavb/shellies-ds9';\n\nimport { Ability, ServiceClass } from './base.ts';\n\n/** DS18B20 operating range upper bound (°C); HAP CurrentTemperature defaults to 100. */\nconst DS18B20_MAX_CELSIUS = 125;\n\n/**\n * Exposes a Shelly temperature component as a HomeKit temperature sensor.\n */\nexport class TemperatureSensorAbility extends Ability {\n constructor(readonly component: Temperature) {\n super(`Temperature ${component.id}`, `temperature-sensor-${component.id}`);\n }\n\n protected get serviceClass(): ServiceClass {\n return this.Service.TemperatureSensor;\n }\n\n protected initialize() {\n const characteristic = this.service.getCharacteristic(this.Characteristic.CurrentTemperature);\n characteristic.setProps({ maxValue: DS18B20_MAX_CELSIUS });\n this.updateTemperature(this.component.tC);\n\n this.component.on('change:tC', this.temperatureChangeHandler, this);\n }\n\n detach() {\n this.component.off('chan