UNPKG

@lucavb/homebridge-shelly-ds9

Version:

Homebridge plugin for the next generation of Shelly devices

1,492 lines (1,469 loc) 68.8 kB
"use strict"; var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // src/platform.ts var import_shellies_ds918 = require("@lucavb/shellies-ds9"); // src/utils/characteristics.ts var createCharacteristics = /* @__PURE__ */ __name((api) => { const _CurrentConsumption = class _CurrentConsumption extends api.hap.Characteristic { constructor() { super("Current Consumption", _CurrentConsumption.UUID, { format: api.hap.Formats.FLOAT, perms: [api.hap.Perms.NOTIFY, api.hap.Perms.READ], unit: "W", minValue: 0, maxValue: 12e3, minStep: 0.1 }); } }; __name(_CurrentConsumption, "CurrentConsumption"); _CurrentConsumption.UUID = "E863F10D-079E-48FF-8F27-9C2605A29F52"; let CurrentConsumption = _CurrentConsumption; const _ElectricCurrent = class _ElectricCurrent extends api.hap.Characteristic { constructor() { super("Electric Current", _ElectricCurrent.UUID, { format: api.hap.Formats.FLOAT, perms: [api.hap.Perms.NOTIFY, api.hap.Perms.READ], unit: "A", minValue: 0, maxValue: 48, minStep: 0.1 }); } }; __name(_ElectricCurrent, "ElectricCurrent"); _ElectricCurrent.UUID = "E863F126-079E-48FF-8F27-9C2605A29F52"; let ElectricCurrent = _ElectricCurrent; const _TotalConsumption = class _TotalConsumption extends api.hap.Characteristic { constructor() { super("Total Consumption", _TotalConsumption.UUID, { format: api.hap.Formats.FLOAT, perms: [api.hap.Perms.NOTIFY, api.hap.Perms.READ], unit: "kWh", minValue: 0, maxValue: 1e6, minStep: 0.1 }); } }; __name(_TotalConsumption, "TotalConsumption"); _TotalConsumption.UUID = "E863F10C-079E-48FF-8F27-9C2605A29F52"; let TotalConsumption = _TotalConsumption; const _Voltage = class _Voltage extends api.hap.Characteristic { constructor() { super("Voltage", _Voltage.UUID, { format: api.hap.Formats.FLOAT, perms: [api.hap.Perms.NOTIFY, api.hap.Perms.READ], unit: "V", minValue: -1e3, maxValue: 1e3, minStep: 0.1 }); } }; __name(_Voltage, "Voltage"); _Voltage.UUID = "E863F10A-079E-48FF-8F27-9C2605A29F52"; let Voltage = _Voltage; return { CurrentConsumption, ElectricCurrent, TotalConsumption, Voltage }; }, "createCharacteristics"); // src/utils/services.ts var createServices = /* @__PURE__ */ __name((api, characteristics) => { const _PowerMeter = class _PowerMeter extends api.hap.Service { constructor(displayName, subtype) { super(displayName, _PowerMeter.UUID, subtype); this.addCharacteristic(characteristics.CurrentConsumption); this.addOptionalCharacteristic(characteristics.TotalConsumption); this.addOptionalCharacteristic(characteristics.ElectricCurrent); this.addOptionalCharacteristic(characteristics.Voltage); } }; __name(_PowerMeter, "PowerMeter"); _PowerMeter.UUID = "DEDBEA44-11ED-429C-BD75-9A2286AA8707"; let PowerMeter = _PowerMeter; const _Pm1 = class _Pm1 extends api.hap.Service { constructor(displayName, subtype) { super(displayName, _Pm1.UUID, subtype); this.addCharacteristic(characteristics.CurrentConsumption); this.addOptionalCharacteristic(characteristics.TotalConsumption); this.addOptionalCharacteristic(characteristics.ElectricCurrent); this.addOptionalCharacteristic(characteristics.Voltage); } }; __name(_Pm1, "Pm1"); _Pm1.UUID = "DEDBEA44-11ED-429C-BD75-9A2286AA8707"; let Pm1 = _Pm1; return { PowerMeter, Pm1 }; }, "createServices"); // src/utils/device-cache.ts var import_fs = require("fs"); var import_path = require("path"); var FILENAME = ".shelly-ng.json"; var SAVE_DELAY = 1e3; var _DeviceCache = class _DeviceCache { /** * @param storagePath - A path to the directory that the devices will be stored in. * @param log - A logging device. */ constructor(storagePath, log) { this.log = log; /** * Holds all devices loaded from the cache file. */ this.devices = /* @__PURE__ */ new Map(); this.saveTimeout = null; this.path = (0, import_path.resolve)(storagePath, FILENAME); } /** * Loads cached devices. */ async load() { this.devices.clear(); try { await import_fs.promises.access(this.path); } catch (e) { this.log.debug("Device cache file " + this.path + " not found"); return; } const data = await import_fs.promises.readFile(this.path, { encoding: "utf8" }); const s = JSON.parse(data); this.log.debug(`Loaded ${s.devices.length} device(s) from cache`); for (const d of s.devices) { this.devices.set(d.id, d); } } /** * Saves the devices to cache. */ async save() { const s = { devices: Array.from(this.devices.values()) }; const data = JSON.stringify(s); this.log.debug(`Saving ${s.devices.length} device(s) to cache`); return import_fs.promises.writeFile(this.path, data); } /** * Saves the devices to cache after a short delay. * Multiple calls to this method within the delay will be debounced. */ saveDelayed() { if (this.saveTimeout !== null) { clearTimeout(this.saveTimeout); } this.saveTimeout = setTimeout(async () => { this.saveTimeout = null; try { await this.save(); } catch (e) { this.log.error("Failed to save devices to cache:", e instanceof Error ? e.message : e); } }, SAVE_DELAY); } /** * Returns device info for the given device ID. */ get(id) { return this.devices.get(id); } /** * Stores the given device info. * @param d - The device info. * @param autoSave - Whether `saveDelayed()` should be automatically invoked. */ set(d, autoSave = true) { this.devices.set(d.id, d); if (autoSave === true) { this.saveDelayed(); } } /** * Stores info about the given device in cache. * @param device - The device. * @param autoSave - Whether `saveDelayed()` should be automatically invoked. */ storeDevice(device, autoSave = true) { const protocol = device.rpcHandler.protocol; let hostname; if (protocol === "websocket") { hostname = device.rpcHandler.hostname; } this.set({ hostname, id: device.id, model: device.model, protocol }, autoSave); } /** * Deletes info about the device with the given ID from cache. * @param id - The device ID. * @param autoSave - Whether `saveDelayed()` should be automatically invoked. */ delete(id, autoSave = true) { this.devices.delete(id); if (autoSave === true) { this.saveDelayed(); } } /** * Returns a new Iterator object that contains each device. */ [Symbol.iterator]() { return this.devices.values(); } }; __name(_DeviceCache, "DeviceCache"); var DeviceCache = _DeviceCache; // src/abilities/base.ts var _Ability = class _Ability { /** * @param serviceName - A name of the service. * @param serviceSubtype - A unique identifier for the service. */ constructor(serviceName, serviceSubtype) { this.serviceName = serviceName; this.serviceSubtype = serviceSubtype; this._platformAccessory = null; this._platform = null; this._log = null; this._service = null; this._active = true; } /** * The associated platform accessory. */ get platformAccessory() { if (this._platformAccessory === null) { throw new Error("Ability has not yet been setup"); } return this._platformAccessory; } /** * A reference to the platform. */ get platform() { if (this._platform === null) { throw new Error("Ability has not yet been setup"); } return this._platform; } /** * A reference to the homebridge API. */ get api() { return this.platform.api; } /** * Shorthand property. */ get Characteristic() { return this.platform.api.hap.Characteristic; } /** * Shorthand property. */ get Service() { return this.platform.api.hap.Service; } /** * Shorthand property. */ get customCharacteristics() { return this.platform.customCharacteristics; } /** * Shorthand property. */ get customServices() { return this.platform.customServices; } /** * The logging device to use. */ get log() { if (this._log === null) { throw new Error("Ability has not yet been setup"); } return this._log; } /** * The HomeKit service that this ability uses. */ get service() { if (this._service === null) { throw new Error("Ability has not yet been setup"); } return this._service; } /** * Whether this ability is active. * Setting an ability to inactive will remove its HomeKit service. */ get active() { return this._active; } set active(value) { if (value === this._active) { return; } this._active = value; this.update(); } /** * Sets up this ability. * This method is called by the parent accessory every time it becomes active. * @param platformAccessory - The homebridge platform accessory to use. * @param platform - A reference to the platform. * @param log - The logger to use. */ setup(platformAccessory, platform, log) { this._platformAccessory = platformAccessory; this._platform = platform; this._log = log; this.update(); } /** * Sets `active` to the given value. * This method can be used when chaining calls, as it returns a reference to `this`. * @param value - Whether the ability should be active. */ setActive(value) { this.active = value; return this; } /** * Determines whether this ability is active. * The default implementation simply returns the value of the `active` property. * Subclasses can override this method to add more conditions. */ isActive() { return this.active; } /** * Updates this ability based on whether it is active. * If active, its service will be added and initialized. * If inactive, its service will be removed. */ update() { if (this._platformAccessory === null) { return; } if (this.isActive()) { if (this._service === null) { this._service = this.addService(); this.initialize(); } } else { if (this._service !== null) { this.detach(); } this.removeService(); this._service = null; } } /** * Returns a service for this ability. * If the platform accessory has a matching service, it will be returned. Otherwise, the service will be added. */ addService() { let service; if (this.serviceName && this.serviceSubtype) { service = this.platformAccessory.getService(this.serviceName) || this.platformAccessory.addService(this.serviceClass, this.serviceName, this.serviceSubtype); } else { service = this.platformAccessory.getService(this.serviceClass); } return service != null ? service : null; } /** * Removes this ability's service from the platform accessory. */ removeService() { let service; if (this._service !== null) { service = this._service; } else if (this.serviceName && this.serviceSubtype) { service = this.platformAccessory.getService(this.serviceName); } else { service = this.platformAccessory.getService(this.serviceClass); } if (service) { this.platformAccessory.removeService(service); } } /** * Helper method that removes a characteristic based on its class (Service.removeCharacteristic() * only accepts an instance). * @param characteristic - The characteristic to remove. */ removeCharacteristic(characteristic) { const s = this.service; if (s.testCharacteristic(characteristic)) { s.removeCharacteristic(s.getCharacteristic(characteristic)); } } /** * Removes all event listeners and all references to the platform accessory. * This method is called by the parent accessory every time it becomes inactive. * Note that this method doesn't remove the service from the platform accessory as it is assumed that * the entire platform accessory is about to be unregistered and discarded. */ destroy() { this.detach(); this._platformAccessory = null; this._platform = null; this._log = null; this._service = null; } }; __name(_Ability, "Ability"); var Ability = _Ability; // src/abilities/accessory-information.ts var _AccessoryInformationAbility = class _AccessoryInformationAbility extends Ability { /** * @param device - The associated device. */ constructor(device) { super(); this.device = device; } get serviceClass() { return this.Service.AccessoryInformation; } initialize() { this.service.setCharacteristic(this.Characteristic.Name, this.platformAccessory.displayName).setCharacteristic(this.Characteristic.Manufacturer, "Allterco").setCharacteristic(this.Characteristic.Model, this.device.modelName).setCharacteristic(this.Characteristic.SerialNumber, this.device.macAddress).setCharacteristic(this.Characteristic.FirmwareRevision, this.device.firmware.version || "1.0.0"); } detach() { } }; __name(_AccessoryInformationAbility, "AccessoryInformationAbility"); var AccessoryInformationAbility = _AccessoryInformationAbility; // src/abilities/cover.ts var names = { door: "Door", window: "Window", windowCovering: "Window Covering" }; var _CoverAbility = class _CoverAbility extends Ability { /** * @param component - The cover component to control. * @param type - The type of cover. */ constructor(component, type = "window") { super(`${names[type]} ${component.id + 1}`, `${type}-${component.id}`); this.component = component; this.type = type; } get serviceClass() { if (this.type === "door") { return this.Service.Door; } else if (this.type === "windowCovering") { return this.Service.WindowCovering; } return this.Service.Window; } /** * The current state of the cover. */ get positionState() { const state = this.component.state; if (state === "opening") { return this.Characteristic.PositionState.INCREASING; } else if (state === "closing") { return this.Characteristic.PositionState.DECREASING; } return this.Characteristic.PositionState.STOPPED; } /** * The current position of the cover. */ get currentPosition() { var _a; return (_a = this.component.current_pos) != null ? _a : 0; } /** * The target position that the cover is moving towards. */ get targetPosition() { var _a; return (_a = this.component.target_pos) != null ? _a : this.currentPosition; } initialize() { if (!this.component.pos_control) { this.log.warn("Only calibrated covers are supported."); return; } this.service.setCharacteristic(this.Characteristic.PositionState, this.positionState).setCharacteristic(this.Characteristic.CurrentPosition, this.currentPosition).setCharacteristic(this.Characteristic.TargetPosition, this.targetPosition); this.service.getCharacteristic(this.Characteristic.TargetPosition).onSet(this.targetPositionSetHandler.bind(this)); this.component.on("change:state", this.stateChangeHandler, this).on("change:current_pos", this.currentPosChangeHandler, this).on("change:target_pos", this.targetPosChangeHandler, this); } detach() { this.component.off("change:state", this.stateChangeHandler, this).off("change:current_pos", this.currentPosChangeHandler, this).off("change:target_pos", this.targetPosChangeHandler, this); } /** * Handles changes to the TargetPosition characteristic. */ async targetPositionSetHandler(value) { if (value === this.component.target_pos) { return; } try { await this.component.goToPosition(value); } catch (e) { this.log.error("Failed to set target position:", e instanceof Error ? e.message : e); throw this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE; } } /** * Handles changes to the `state` property. */ stateChangeHandler() { this.log.debug(`${this.component.id} state changed to ${this.positionState}`, { target: this.targetPosition, current: this.currentPosition }); this.updateStates(); } /** * Updates all states. * * Shelly does not send all attributes in a single notification. * We apparently need to update all states when any of them change, otherwise HomeKit * gets confused and thinks the cover is in a different state than it actually is. */ updateStates() { this.service.getCharacteristic(this.Characteristic.PositionState).updateValue(this.positionState); this.service.getCharacteristic(this.Characteristic.TargetPosition).updateValue(this.targetPosition); this.service.getCharacteristic(this.Characteristic.CurrentPosition).updateValue(this.currentPosition); } /** * Handles changes to the `current_pos` property. */ currentPosChangeHandler() { this.log.debug(`${this.component.id} position changed to ${this.currentPosition}`, { target: this.targetPosition, state: this.positionState }); this.updateStates(); this.service.getCharacteristic(this.Characteristic.TargetPosition).updateValue(this.currentPosition); } /** * Handles changes to the `target_pos` property. */ targetPosChangeHandler() { this.log.debug(`${this.component.id} target position changed to ${this.targetPosition}`, { state: this.positionState, current: this.currentPosition }); this.updateStates(); } }; __name(_CoverAbility, "CoverAbility"); var CoverAbility = _CoverAbility; // src/abilities/light.ts var _LightAbility = class _LightAbility extends Ability { /** * @param component - The light component to control. */ constructor(component) { super(`Light ${component.id + 1}`, `light-${component.id}`); this.component = component; } get serviceClass() { return this.Service.Lightbulb; } initialize() { this.service.setCharacteristic(this.Characteristic.On, this.component.output); this.service.getCharacteristic(this.Characteristic.On).onSet(this.onSetHandler.bind(this)); this.service.getCharacteristic(this.Characteristic.Brightness).onSet(this.brightnessSetHandler.bind(this)); this.component.on("change:output", this.outputChangeHandler, this); this.component.on("change:brightness", this.brightnessChangeHandler, this); } detach() { this.component.off("change:output", this.outputChangeHandler, this); this.component.off("change:brightness", this.brightnessChangeHandler, this); } /** * Handles changes to the Light.On characteristic. */ async onSetHandler(value) { if (value === this.component.output) { return; } try { await this.component.set(value); } catch (e) { this.log.error("Failed to set light:", e instanceof Error ? e.message : e); throw this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE; } } /** * Handles changes to the `output` property. */ outputChangeHandler(value) { if (value) { this.log.info("Light Status(" + this.component.id + "): on"); } else { this.log.info("Light Status(" + this.component.id + "): off"); } this.service.getCharacteristic(this.Characteristic.On).updateValue(value); } /** * Handles changes to the Light.Brightness characteristic. */ async brightnessSetHandler(value) { if (value === this.component.brightness) { return; } try { await this.component.set(void 0, value); } catch (e) { this.log.error("Failed to set light:", e instanceof Error ? e.message : e); throw this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE; } } /** * Handles changes to the `brightness` property. */ brightnessChangeHandler(value) { this.log.info("Light Status(" + this.component.id + "): " + value); this.service.getCharacteristic(this.Characteristic.Brightness).updateValue(value); } }; __name(_LightAbility, "LightAbility"); var LightAbility = _LightAbility; // src/abilities/outlet.ts var _OutletAbility = class _OutletAbility extends Ability { /** * @param component - The switch component to control. */ constructor(component) { super(`Outlet ${component.id + 1}`, `outlet-${component.id}`); this.component = component; } get serviceClass() { return this.Service.Outlet; } initialize() { this.service.setCharacteristic(this.Characteristic.On, this.component.output).setCharacteristic( this.Characteristic.OutletInUse, this.component.apower !== void 0 && this.component.apower !== 0 ); this.service.getCharacteristic(this.Characteristic.On).onSet(this.onSetHandler.bind(this)); this.component.on("change:output", this.outputChangeHandler, this).on("change:apower", this.apowerChangeHandler, this); } detach() { this.component.off("change:output", this.outputChangeHandler, this).off("change:apower", this.apowerChangeHandler, this); } /** * Handles changes to the Outlet.On characteristic. */ async onSetHandler(value) { if (value === this.component.output) { return; } try { await this.component.set(value); } catch (e) { this.log.error("Failed to set switch:", e instanceof Error ? e.message : e); throw this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE; } } /** * Handles changes to the `output` property. */ outputChangeHandler(value) { if (value) { this.log.info("Switch Status(" + this.component.id + "): on"); } else { this.log.info("Switch Status(" + this.component.id + "): off"); } this.service.getCharacteristic(this.Characteristic.On).updateValue(value); } /** * Handles changes to the `apower` property. */ apowerChangeHandler(value) { this.service.getCharacteristic(this.Characteristic.OutletInUse).updateValue(value); } }; __name(_OutletAbility, "OutletAbility"); var OutletAbility = _OutletAbility; // src/abilities/power-meter.ts var _PowerMeterAbility = class _PowerMeterAbility extends Ability { /** * @param component - The switch or cover component to get readings from. */ constructor(component) { super(`Power Meter ${component.id + 1}`, `power-meter-${component.id}`); this.component = component; } get serviceClass() { return this.customServices.PowerMeter; } initialize() { var _a; const s = this.service; const c = this.component; const cc = this.customCharacteristics; s.setCharacteristic(cc.CurrentConsumption, (_a = c.apower) != null ? _a : 0); c.on("change:apower", this.apowerChangeHandler, this); if (c.voltage !== void 0) { s.setCharacteristic(cc.Voltage, c.voltage); c.on("change:voltage", this.voltageChangeHandler, this); } else { this.removeCharacteristic(cc.Voltage); } if (c.current !== void 0) { s.setCharacteristic(cc.ElectricCurrent, c.current); c.on("change:current", this.currentChangeHandler, this); } else { this.removeCharacteristic(cc.ElectricCurrent); } if (c.aenergy !== void 0) { s.setCharacteristic(cc.TotalConsumption, c.aenergy.total / 1e3); c.on("change:aenergy", this.aenergyChangeHandler, this); } else { this.removeCharacteristic(cc.TotalConsumption); } } detach() { this.component.off("change:apower", this.apowerChangeHandler, this).off("change:voltage", this.voltageChangeHandler, this).off("change:current", this.currentChangeHandler, this).off("change:aenergy", this.aenergyChangeHandler, this); } /** * Handles changes to the `apower` property. */ apowerChangeHandler(value) { const positiveValue = Math.max(0, value); this.service.updateCharacteristic(this.customCharacteristics.CurrentConsumption, positiveValue); } /** * Handles changes to the `voltage` property. */ voltageChangeHandler(value) { this.service.updateCharacteristic(this.customCharacteristics.Voltage, value); } /** * Handles changes to the `current` property. */ currentChangeHandler(value) { this.service.updateCharacteristic(this.customCharacteristics.ElectricCurrent, value); } /** * Handles changes to the `aenergy` property. */ aenergyChangeHandler(value) { const attr = value; this.service.updateCharacteristic(this.customCharacteristics.TotalConsumption, attr.total / 1e3); } }; __name(_PowerMeterAbility, "PowerMeterAbility"); var PowerMeterAbility = _PowerMeterAbility; // src/abilities/readonly-switch.ts var import_homebridge = require("homebridge"); var _ReadonlySwitchAbility = class _ReadonlySwitchAbility extends Ability { /** * @param component - The input component to represent. */ constructor(component) { super(`Switch ${component.id + 1}`, `readonly-switch-${component.id}`); this.component = component; } get serviceClass() { return this.Service.Switch; } initialize() { var _a; this.service.getCharacteristic(this.Characteristic.On).setProps({ perms: [import_homebridge.Perms.NOTIFY, import_homebridge.Perms.PAIRED_READ] }).setValue((_a = this.component.state) != null ? _a : false); this.component.on("change:state", this.stateChangeHandler, this); } detach() { this.component.off("change:state", this.stateChangeHandler, this); } /** * Handles changes to the `state` property. */ stateChangeHandler(value) { const v = value === null ? false : value; if (value) { this.log.info("Switch Status(" + this.component.id + "): on"); } else { this.log.info("Switch Status(" + this.component.id + "): off"); } this.service.getCharacteristic(this.Characteristic.On).updateValue(v); } }; __name(_ReadonlySwitchAbility, "ReadonlySwitchAbility"); var ReadonlySwitchAbility = _ReadonlySwitchAbility; // src/abilities/service-label.ts var _ServiceLabelAbility = class _ServiceLabelAbility extends Ability { /** * @param namespace - The naming schema for the accessory. */ constructor(namespace = "arabicNumerals") { super(); this.namespace = namespace; } get serviceClass() { return this.Service.ServiceLabel; } initialize() { const SLN = this.Characteristic.ServiceLabelNamespace; this.service.setCharacteristic(SLN, this.namespace === "dots" ? SLN.DOTS : SLN.ARABIC_NUMERALS); } detach() { } }; __name(_ServiceLabelAbility, "ServiceLabelAbility"); var ServiceLabelAbility = _ServiceLabelAbility; // src/abilities/stateless-programmable-switch.ts var _StatelessProgrammableSwitchAbility = class _StatelessProgrammableSwitchAbility extends Ability { /** * @param component - The input component to control. */ constructor(component) { super(`Button ${component.id + 1}`, `stateless-programmable-switch-${component.id}`); this.component = component; } get serviceClass() { return this.Service.StatelessProgrammableSwitch; } initialize() { this.service.setCharacteristic(this.Characteristic.ServiceLabelIndex, this.component.id + 1); this.component.on("singlePush", this.singlePushHandler, this).on("doublePush", this.doublePushHandler, this).on("longPush", this.longPushHandler, this); } detach() { this.component.off("singlePush", this.singlePushHandler, this).off("doublePush", this.doublePushHandler, this).off("longPush", this.longPushHandler, this); } /** * Triggers a button press event. * @param type - The type of button press to trigger. */ triggerPress(type) { this.log.debug(`Input ${this.component.id}: ${type} press`); const PSE = this.Characteristic.ProgrammableSwitchEvent; let value; switch (type) { case "single" /* Single */: value = PSE.SINGLE_PRESS; break; case "double" /* Double */: value = PSE.DOUBLE_PRESS; break; case "long" /* Long */: value = PSE.LONG_PRESS; break; } this.service.getCharacteristic(this.Characteristic.ProgrammableSwitchEvent).updateValue(value); } /** * Handles 'singlePush' events from our input component. */ singlePushHandler() { this.triggerPress("single" /* Single */); } /** * Handles 'doublePush' events from our input component. */ doublePushHandler() { this.triggerPress("double" /* Double */); } /** * Handles 'longPush' events from our input component. */ longPushHandler() { this.triggerPress("long" /* Long */); } }; __name(_StatelessProgrammableSwitchAbility, "StatelessProgrammableSwitchAbility"); var StatelessProgrammableSwitchAbility = _StatelessProgrammableSwitchAbility; // src/abilities/switch.ts var _SwitchAbility = class _SwitchAbility extends Ability { /** * @param component - The switch component to control. */ constructor(component) { super(`Switch ${component.id + 1}`, `switch-${component.id}`); this.component = component; } get serviceClass() { return this.Service.Switch; } initialize() { this.service.setCharacteristic(this.Characteristic.On, this.component.output); this.service.getCharacteristic(this.Characteristic.On).onSet(this.onSetHandler.bind(this)); this.component.on("change:output", this.outputChangeHandler, this); } detach() { this.component.off("change:output", this.outputChangeHandler, this); } /** * Handles changes to the Switch.On characteristic. */ async onSetHandler(value) { if (value === this.component.output) { return; } try { await this.component.set(value); } catch (e) { this.log.error("Failed to set switch:", e instanceof Error ? e.message : e); throw this.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE; } } /** * Handles changes to the `output` property. */ outputChangeHandler(value) { if (value) { this.log.info("Switch Status(" + this.component.id + "): on"); } else { this.log.info("Switch Status(" + this.component.id + "): off"); } this.service.getCharacteristic(this.Characteristic.On).updateValue(value); } }; __name(_SwitchAbility, "SwitchAbility"); var SwitchAbility = _SwitchAbility; // src/abilities/pm1.ts var _Pm1Ability = class _Pm1Ability extends Ability { /** * @param componentPm1 - The switch or cover component to get readings from. */ constructor(componentPm1) { super(`Pm1 ${componentPm1.id + 1}`, `Pm1-${componentPm1.id}`); this.componentPm1 = componentPm1; } get serviceClass() { return this.customServices.Pm1; } initialize() { var _a; const s = this.service; const cPm1 = this.componentPm1; const cc = this.customCharacteristics; s.setCharacteristic(cc.CurrentConsumption, (_a = cPm1.apower) != null ? _a : 0); s.setCharacteristic(this.Characteristic.On, false); cPm1.on("change:apower", this.apowerChangeHandler, this); if (cPm1.voltage !== void 0) { s.setCharacteristic(cc.Voltage, cPm1.voltage); cPm1.on("change:voltage", this.voltageChangeHandler, this); } else { this.removeCharacteristic(cc.Voltage); } if (cPm1.current !== void 0) { s.setCharacteristic(cc.ElectricCurrent, cPm1.current); cPm1.on("change:current", this.currentChangeHandler, this); } else { this.removeCharacteristic(cc.ElectricCurrent); } if (cPm1.aenergy !== void 0) { s.setCharacteristic(cc.TotalConsumption, cPm1.aenergy.total / 1e3); cPm1.on("change:aenergy", this.aenergyChangeHandler, this); } else { this.removeCharacteristic(cc.TotalConsumption); } } detach() { this.componentPm1.off("change:apower", this.apowerChangeHandler, this).off("change:voltage", this.voltageChangeHandler, this).off("change:current", this.currentChangeHandler, this).off("change:aenergy", this.aenergyChangeHandler, this); } /** * Handles changes to the `apower` property. */ apowerChangeHandler(value) { this.service.updateCharacteristic(this.customCharacteristics.CurrentConsumption, value); if (typeof value === "number") { if (value >= 1) { this.service.updateCharacteristic(this.Characteristic.On, true); } else { this.service.updateCharacteristic(this.Characteristic.On, false); } } } /** * Handles changes to the `voltage` property. */ voltageChangeHandler(value) { this.service.updateCharacteristic(this.customCharacteristics.Voltage, value); } /** * Handles changes to the `current` property. */ currentChangeHandler(value) { this.service.updateCharacteristic(this.customCharacteristics.ElectricCurrent, value); } /** * Handles changes to the `aenergy` property. */ aenergyChangeHandler(value) { const attr = value; this.service.updateCharacteristic(this.customCharacteristics.TotalConsumption, attr.total / 1e3); } }; __name(_Pm1Ability, "Pm1Ability"); var Pm1Ability = _Pm1Ability; // src/accessory.ts var _Accessory = class _Accessory { /** * @param id - The accessory ID. * @param deviceId - The associated device ID. * @param name - A user-friendly name of the accessory. * @param platform - A reference to the homebridge platform. * @param log - The logger to use. * @param abilities - The abilities that this accessory has. */ constructor(id, deviceId, name, platform, log, ...abilities) { this.id = id; this.deviceId = deviceId; this.name = name; this.platform = platform; this.log = log; this._active = true; /** * Timeout used to delay calls to `update()`. */ this.updateTimeout = null; this.uuid = platform.api.hap.uuid.generate(`${deviceId}-${id}`); this.abilities = abilities; this._platformAccessory = platform.getAccessory(this.uuid) || null; if (this._platformAccessory !== null) { log.debug(`Accessory loaded from cache (ID: ${id})`); } this.update(); } /** * The underlying homebridge platform accessory. * This property will be `null` when the accessory is inactive. */ get platformAccessory() { return this._platformAccessory; } /** * Whether this accessory is active. * Setting an accessory to inactive will remove it from HoneKit. */ get active() { return this._active; } set active(value) { if (value === this._active) { return; } this._active = value; this.update(); } /** * Sets `active` to the given value. * This method can be used when chaining calls, as it returns a reference to `this`. * @param value - Whether the accessory should be active. */ setActive(value) { this.active = value; return this; } /** * Updates this accessory based on whether it is active. */ update() { if (this.updateTimeout !== null) { clearTimeout(this.updateTimeout); } this.updateTimeout = setTimeout(() => { this.updateTimeout = null; if (this.active) { this.activate(); } else { this.deactivate(); } }, 0); } /** * Activates this accessory, by creating a platform accessory and setting up all abilities. */ activate() { if (this._platformAccessory === null) { this._platformAccessory = this.createPlatformAccessory(); this.log.debug(`Accessory activated (ID: ${this.id})`); } for (const a of this.abilities) { try { a.setup(this._platformAccessory, this.platform, this.log); } catch (e) { this.log.error("Failed to setup ability:", e instanceof Error ? e.message : e); this.log.debug("Accessory ID:", this.id); if (e instanceof Error && e.stack) { this.log.debug(e.stack); } } } this.platform.addAccessory(this._platformAccessory); } /** * Deactivates this accessory, by destroying all abilities and the platform accessory. */ deactivate() { for (const a of this.abilities) { try { a.destroy(); } catch (e) { this.log.error("Failed to destroy ability:", e instanceof Error ? e.message : e); this.log.debug("Accessory ID:", this.id); if (e instanceof Error && e.stack) { this.log.debug(e.stack); } } } if (this._platformAccessory !== null) { this.platform.removeAccessory(this._platformAccessory); this._platformAccessory = null; this.log.debug(`Accessory deactivated (ID: ${this.id})`); } } /** * Creates a new platform accessory for this accessory. */ createPlatformAccessory() { const pa = new this.platform.api.platformAccessory(this.name, this.uuid); pa.context.device = { id: this.deviceId }; return pa; } /** * Removes all event listeners from this accessory. */ detach() { if (this.updateTimeout !== null) { clearTimeout(this.updateTimeout); this.updateTimeout = null; } for (const a of this.abilities) { try { a.detach(); } catch (e) { this.log.error("Failed to detach ability:", e instanceof Error ? e.message : e); this.log.debug("Accessory ID:", this.id); if (e instanceof Error && e.stack) { this.log.debug(e.stack); } } } } }; __name(_Accessory, "Accessory"); var Accessory = _Accessory; // src/utils/device-logger.ts var import_homebridge2 = require("homebridge"); var _DeviceLogger = class _DeviceLogger { /** * @param device - The device to use. * @param deviceName - A user-friendly name of the device. * @param logger - The logging device to write to. */ constructor(device, deviceName, logger) { this.device = device; this.logger = logger; this.prefix = `[${deviceName || device.id}] `; } info(message, ...parameters) { this.log(import_homebridge2.LogLevel.INFO, message, ...parameters); } warn(message, ...parameters) { this.log(import_homebridge2.LogLevel.WARN, message, ...parameters); } error(message, ...parameters) { this.log(import_homebridge2.LogLevel.ERROR, message, ...parameters); } debug(message, ...parameters) { this.log(import_homebridge2.LogLevel.DEBUG, message, ...parameters); } log(level, message, ...parameters) { this.logger.log(level, this.prefix + message, ...parameters); } }; __name(_DeviceLogger, "DeviceLogger"); var DeviceLogger = _DeviceLogger; // src/device-delegates/base.ts var _DeviceDelegate = class _DeviceDelegate { /** * @param device - The device to handle. * @param options - Configuration options for the device. * @param platform - A reference to the homebridge platform. */ constructor(device, options, platform) { this.device = device; this.options = options; this.platform = platform; /** * Holds all accessories for this device. */ this.accessories = /* @__PURE__ */ new Map(); this.log = new DeviceLogger(device, options.name, platform.log); this.log.info("Device added"); this.log.debug(device.rpcHandler.connected ? "Device is connected" : "Device is disconnected"); this.connected = device.rpcHandler.connected; device.rpcHandler.on("connect", this.handleConnect, this).on("disconnect", this.handleDisconnect, this).on("request", this.handleRequest, this); this.setup(); } /** * Registers a device delegate, so that it can later be found based on a device class or model * using the `DeviceDelegate.getDelegate()` method. * @param delegate - A subclass of `DeviceDelegate`. * @param deviceClasses - One or more subclasses of `Device`. */ static registerDelegate(delegate, ...deviceClasses) { for (const deviceCls of deviceClasses) { const mdl = deviceCls.model.toUpperCase(); if (_DeviceDelegate.delegates.has(mdl)) { throw new Error(`A device delegate for ${deviceCls.model} has already been registered`); } _DeviceDelegate.delegates.set(mdl, delegate); } } /** * Returns the device delegate for the given device class or model, if one has been registered. * @param deviceClsOrModel - The device class or model ID to lookup. */ static getDelegate(deviceClsOrModel) { const mdl = typeof deviceClsOrModel === "string" ? deviceClsOrModel : deviceClsOrModel.model; return _DeviceDelegate.delegates.get(mdl.toUpperCase()); } /** * Retrieves configuration options for the given component from the device options. * @param component - The component. * @returns A set of options, if found. */ getComponentOptions(component) { var _a; return (_a = this.options) == null ? void 0 : _a[component.key]; } /** * Creates an accessory with the given ID. * If a matching platform accessory is not found in cache, a new one will be created. * @param id - A unique identifier for this accessory. * @param nameSuffix - A string to append to the name of this accessory. * @param abilities - The abilities to add to this accessory. */ createAccessory(id, nameSuffix, ...abilities) { if (this.accessories.has(id)) { throw new Error(`An accessory with ID '${id}' already exists`); } let name = this.options.name || this.device.modelName; if (nameSuffix) { name += " " + nameSuffix; } const accessory = new Accessory( id, this.device.id, name, this.platform, this.log, new AccessoryInformationAbility(this.device), ...abilities ); this.accessories.set(id, accessory); return accessory; } /** * Creates an accessory for a switch component. * @param swtch - The switch component to use. * @param opts - Options for the switch. */ addSwitch(swtch, opts) { var _a; const o = opts != null ? opts : {}; const switchOpts = (_a = this.getComponentOptions(swtch)) != null ? _a : {}; const type = typeof switchOpts.type === "string" ? switchOpts.type.toLowerCase() : "switch"; const isOutlet = type === "outlet"; const id = o.single === true ? "switch" : `switch-${swtch.id}`; const nameSuffix = o.single === true ? null : `Switch ${swtch.id + 1}`; return this.createAccessory( id, nameSuffix, new OutletAbility(swtch).setActive(isOutlet), new SwitchAbility(swtch).setActive(!isOutlet), // use the apower property to determine whether power metering is available new PowerMeterAbility(swtch).setActive(swtch.apower !== void 0) ).setActive(switchOpts.exclude !== true && o.active !== false); } /** * Creates an accessory for a cover component. * @param cover - The cover component to use. * @param opts - Options for the cover. */ addCover(cover, opts) { var _a; const o = opts != null ? opts : {}; const coverOpts = (_a = this.getComponentOptions(cover)) != null ? _a : {}; const type = typeof coverOpts.type === "string" ? coverOpts.type.toLowerCase() : "window"; const isDoor = type === "door"; const isWindowCovering = type === "windowcovering"; const id = o.single === true ? "cover" : `cover-${cover.id}`; return this.createAccessory( id, "Cover", new CoverAbility(cover, "door").setActive(isDoor), new CoverAbility(cover, "windowCovering").setActive(isWindowCovering), new CoverAbility(cover, "window").setActive(!isDoor && !isWindowCovering), new PowerMeterAbility(cover) ).setActive(coverOpts.exclude !== true && o.active !== false); } /** * Creates an accessory for a light component. * @param light - The light component to use. * @param opts - Options for the light. */ addLight(light, opts) { var _a; const o = opts != null ? opts : {}; const lightOpts = (_a = this.getComponentOptions(light)) != null ? _a : {}; const id = o.single === true ? "light" : `light-${light.id}`; const nameSuffix = o.single === true ? null : `Light ${light.id + 1}`; return this.createAccessory(id, nameSuffix, new LightAbility(light)).setActive( lightOpts.exclude !== true && o.active !== false ); } /** * Handles 'connect' events from the RPC handler. */ handleConnect() { this.log.info("Device connected"); this.connected = true; } /** * Handles 'disconnect' events from the RPC handler. */ handleDisconnect(code, reason, reconnectIn) { const details = reason.length > 0 ? "reason: " + reason : "code: " + code; this.log.warn((this.connected ? "Device disconnected" : "Connection failed") + " (" + details + ")"); if (reconnectIn !== null) { let msg = "Reconnecting in "; if (reconnectIn < 60 * 1e3) { msg += Math.floor(reconnectIn / 1e3) + " second(s)"; } else if (reconnectIn < 60 * 60 * 1e3) { msg += Math.floor(reconnectIn / (60 * 1e3)) + " minute(s)"; } else { msg += Math.floor(reconnectIn / (60 * 60 * 1e3)) + " hour(s)"; } this.log.info(msg); } this.connected = false; } /** * Handles 'request' events from the RPC handler. */ handleRequest(method) { this.log.debug("WebSocket:", method); } /** * Removes all event listeners from this device. */ detach() { this.device.rpcHandler.off("connect", this.handleConnect, this).off("disconnect", this.handleDisconnect, this).off("request", this.handleRequest, this); for (const a of this.accessories.values()) { a.detach(); } } /** * Destroys this device delegate, removing all event listeners and unregistering all accessories. */ destroy() { this.detach(); const pas = Array.from(this.accessories.values()).map((a) => a.platformAccessory).filter((a) => a !== null); if (pas.length > 0) { this.platform.removeAccessory(...pas); } this.log.info("Device removed"); } }; __name(_DeviceDelegate, "DeviceDelegate"); /** * Holds all registered delegates. */ _DeviceDelegate.delegates = /* @__PURE__ */ new Map(); var DeviceDelegate = _DeviceDelegate; // src/device-delegates/shelly-plus-1.ts var import_shellies_ds9 = require("@lucavb/shellies-ds9"); var _ShellyPlus1Delegate = class _ShellyPlus1Delegate extends DeviceDelegate { setup() { const d = this.device; this.addSwitch(d.switch0, { single: true }); } }; __name(_ShellyPlus1Delegate, "ShellyPlus1Delegate"); var ShellyPlus1Delegate = _ShellyPlus1Delegate; DeviceDelegate.registerDelegate( ShellyPlus1Delegate, import_shellies_ds9.ShellyPlus1, import_shellies_ds9.ShellyPlus1Ul, import_shellies_ds9.ShellyPlus1V3, import_shellies_ds9.ShellyPlus1Mini, import_shellies_ds9.ShellyPlus1MiniV3 ); // src/device-delegates/shelly-plus-1-pm.ts var import_shellies_ds92 = require("@lucavb/shellies-ds9"); var _ShellyPlus1PmDelegate = class _ShellyPlus1PmDelegate extends DeviceDelegate { setup() { const d = this.device; this.addSwitch(d.switch0, { single: true }); } }; __name(_ShellyPlus1PmDelegate, "ShellyPlus1PmDelegate"); var ShellyPlus1PmDelegate = _ShellyPlus1PmDelegate; DeviceDelegate.registerDelegate( ShellyPlus1PmDelegate, import_shellies_ds92.ShellyPlus1Pm, import_shellies_ds92.ShellyPlus1PmUl, import_shellies_ds92.ShellyPlus1PmV3, import_shellies_ds92.ShellyPlus1PmMini, import_shellies_ds92.ShellyPlus1PmMiniV3 ); // src/device-delegates/shelly-plus-pm.ts var import_shellies_ds93 = require("@lucavb/shellies-ds9"); var _ShellyPlusPmDelegate = class _ShellyPlusPmDelegate extends DeviceDelegate { setup() { const d = this.device; this.createAccessory("switch", this.device.id, new Pm1Ability(d.pm1)); } }; __name(_ShellyPlusPmDelegate, "ShellyPlusPmDelegate"); var ShellyPlusPmDelegate = _ShellyPlusPmDelegate; DeviceDelegate.registerDelegate(ShellyPlusPmDelegate, import_shellies_ds93.ShellyPlusPmMini, import_shellies_ds93.ShellyPlusPmMiniV3); // src/device-delegates/shelly-plus-2-pm.ts var import_shellies_ds94 = require("@lucavb/shellies-ds9"); var _ShellyPlus2PmDelegate = class _ShellyPlus2PmDelegate extends DeviceDelegate { setup() { const d = this.device; const isCover = d.profile === "cover"; this.addCover(d.cover0, { active: isCover }); this.addSwitch(d.switch0, { active: !isCover }); this.addSwitch(d.switch1, { active: !isCover }); } }; __name(_ShellyPlus2PmDelegate, "ShellyPlus2PmDelegate"); var ShellyPlus2PmDelegate = _ShellyPlus2PmDelegate; DeviceDelegate.registerDelegate(ShellyPlus2PmDelegate, import_shellies_ds94.ShellyPlus2Pm, import_shellies_ds94.ShellyPlus2PmRev1, import_shellies_ds94.ShellyGen32Pm); // src/device-delegates/shelly-plus-i4.ts var import_shellies_ds95 = require("@lucavb/shellies-ds9"); var _ShellyPlusI4Delegate = class _ShellyPlusI4Delegate extends DeviceDelegate { setup() { var _a, _b, _c, _d; const d = this.device; const input0IsButton = ((_a = d.input0.config) == null ? void 0 : _a.type) === "button"; const input1IsButton = ((_b = d.input1.config) == null ? void 0 : _b.type) === "button"; const input2IsButton = ((_c = d.input2.config) == null ? void 0 : _c.type) === "button"; const input3IsButton = ((_d = d.input3.config) == null ? void 0 : _d.type) === "button"; this.createAccessory( "buttons", null, new StatelessProgrammableSwitchAbility(d.input0).setActive(input0IsButton), new StatelessProgrammableSwitchAbility(d.input1).setActive(input1IsButton), new StatelessProgrammableSwitchAbility(d.input2).setActive(input2IsButton), new StatelessProgrammableSwitchAbility(d.input3).setActive(input3IsButton), new ServiceLabelAbility() ).setActive(input0IsButton || input1IsButton || input2IsButton || input3IsButton); this.createAccessory("switch0", null, new ReadonlySwitchAbility(d.input0)).setActive(!input0IsButton); this.createAccessory("switch1", null, new ReadonlySwitchAbility(d.input1