@lucavb/homebridge-shelly-ds9
Version:
Homebridge plugin for the next generation of Shelly devices
1,492 lines (1,469 loc) • 68.8 kB
JavaScript
"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