UNPKG

shellies-ds9

Version:

Handles communication with the next generation of Shelly devices

270 lines 9.68 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Shellies = void 0; const eventemitter3_1 = __importDefault(require("eventemitter3")); const devices_1 = require("./devices"); const rpc_1 = require("./rpc"); /** * Default options for discovered devices. */ const DEFAULT_DEVICE_OPTIONS = { exclude: false, protocol: 'websocket', }; /** * Default `Shellies` options. */ const DEFAULT_SHELLIES_OPTIONS = { autoLoadStatus: true, autoLoadConfig: false, deviceOptions: null, }; /** * This is the main class for the shellies-ds9 library. * This class manages a list of Shelly devices. New devices can be added by registering a device discoverer. */ class Shellies extends eventemitter3_1.default { /** * @param opts - A set of configuration options. */ constructor(opts) { super(); /** * Factory used to create new `WebSocketRpcHandler`s. */ this.websocket = new rpc_1.WebSocketRpcHandlerFactory(); /** * Holds all devices, mapped to their IDs for quick and easy access. */ this.devices = new Map(); /** * Event handlers bound to `this`. */ this.discoverHandler = this.handleDiscoveredDevice.bind(this); /** * Holds IDs of devices that have been discovered but not yet added. */ this.pendingDevices = new Set(); /** * Holds IDs of devices that have been discovered but are excluded or whose * model designation isn't recognized. */ this.ignoredDevices = new Set(); // store the options, with default values this.options = { ...DEFAULT_SHELLIES_OPTIONS, ...(opts || {}) }; } /** * The number of devices. */ get size() { return this.devices.size; } /** * Adds a device. If a device with the same ID has already been added, an * error will be thrown. * @param device - The device to add. */ add(device) { // make sure we don't have a device with the same ID if (this.devices.has(device.id)) { throw new Error(`Device with ID ${device.id} already added`); } // make sure its not marked as pending this.pendingDevices.delete(device.id); // add the device this.devices.set(device.id, device); // emit an `add` event this.emit('add', device); return this; } /** * Determines whether a device has been added. * @param deviceOrId - The device or device ID to test. * @returns `true` if the device has been added; `false` otherwise. */ has(deviceOrId) { const id = deviceOrId instanceof devices_1.Device ? deviceOrId.id : deviceOrId; return this.devices.has(id); } /** * Returns the device with the given ID, or `undefined` if no such device was * found. */ get(deviceId) { return this.devices.get(deviceId); } /** * Executes a provided function once for each device. * @param callback - Function to execute for each device. * @param thisArg - Value to be used as `this` when executing `callback`. */ forEach(callback, thisArg) { this.devices.forEach((device, id) => { callback.call(thisArg, device, id, this); }); } /** * Returns a new Iterator object that contains an array of * `[DeviceId, Device]` for each device. */ entries() { return this.devices.entries(); } /** * Returns a new Iterator object that contains the device IDs for each device. */ keys() { return this.devices.keys(); } /** * Returns a new Iterator object that contains each device. */ values() { return this.devices.values(); } /** * Returns a new Iterator object that contains each device. */ [Symbol.iterator]() { return this.devices.values(); } /** * Removes a device. * @param deviceOrId - The device or ID of the device to remove. * @returns `true` if a device has been removed; `false` otherwise. */ delete(deviceOrId) { const id = deviceOrId instanceof devices_1.Device ? deviceOrId.id : deviceOrId; const device = this.devices.get(id); if (device !== undefined) { this.devices.delete(id); // emit a `remove` event this.emit('remove', device); return true; } return false; } /** * Removes all devices. */ clear() { // emit `remove` events for all devices for (const [, device] of this.devices) { this.emit('remove', device); } this.devices.clear(); } /** * Registers a device discoverer, making discovered devices be added to this library. * @param discoverer - The discoverer to register. */ registerDiscoverer(discoverer) { discoverer.on('discover', this.discoverHandler); } /** * Unregisters a previously registered device discoverer. * @param discoverer - The discoverer to unregister. */ unregisterDiscoverer(discoverer) { discoverer.removeListener('discover', this.discoverHandler); } /** * Retrieves configuration options for the device with the given ID. * @param deviceId - Device ID. */ getDeviceOptions(deviceId) { // get all options (with defaults) let opts = undefined; const deviceOptions = this.options.deviceOptions; if (deviceOptions instanceof Map) { opts = deviceOptions.get(deviceId); } else if (typeof deviceOptions === 'function') { opts = deviceOptions(deviceId); } return { ...DEFAULT_DEVICE_OPTIONS, ...(opts || {}) }; } /** * Creates an `RpcHandler` for a device. * @param identifiers - A set of device identifiers. * @param options - Configuration options for the device. */ createRpcHandler(identifiers, options) { if (options.protocol === 'websocket' && identifiers.hostname) { const opts = { ...this.options.websocket, password: options.password }; return this.websocket.create(identifiers.hostname, opts); } // we're missing something throw new Error(`Missing required device identifier(s) (device ID: ${identifiers.deviceId}, protocol: ${options.protocol})`); } /** * Handles 'discover' events from device discoverers. */ async handleDiscoveredDevice(identifiers) { var _a; const deviceId = identifiers.deviceId; if (this.devices.has(deviceId) || this.pendingDevices.has(deviceId) || this.ignoredDevices.has(deviceId)) { // ignore if we've seen this device before return; } // get the configuration options for this device const opts = this.getDeviceOptions(deviceId); if (opts.exclude) { // exclude this device this.ignoredDevices.add(deviceId); this.emit('exclude', deviceId); return; } this.pendingDevices.add(deviceId); try { // create an RPC handler const rpcHandler = this.createRpcHandler(identifiers, opts); // load info about this device const info = await rpcHandler.request('Shelly.GetDeviceInfo'); // make sure the returned device ID matches if (info.id.toLowerCase() !== deviceId.toLowerCase()) { throw new Error(`Unexpected device ID (returned: ${info.id}, expected: ${deviceId})`); } // get the device class for this model const cls = devices_1.Device.getClass((_a = info.model) !== null && _a !== void 0 ? _a : ''); if (cls === undefined) { // abort if we don't have a matching device class this.ignoredDevices.add(deviceId); this.emit('unknown', deviceId, info.model, identifiers); return; } // create the device const device = new cls(info, rpcHandler); if (this.options.autoLoadStatus === true) { // load its status await device.loadStatus(); } if (this.options.autoLoadConfig === true) { // load its config await device.loadConfig(); } this.pendingDevices.delete(deviceId); // add the device this.add(device); } catch (e) { this.pendingDevices.delete(deviceId); // create a custom Error const message = e instanceof Error ? e.message : String(e); const error = new Error(`Failed to add discovered device (id: ${deviceId}): ${message}`); if (e instanceof Error) { error.stack = e.stack; } else { Error.captureStackTrace(error); } // emit the error this.emit('error', deviceId, error); } } } exports.Shellies = Shellies; //# sourceMappingURL=shellies.js.map