UNPKG

homebridge-unifi-smartpower

Version:
356 lines 17.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.UniFiSmartPower = exports.UniFiSwitchPortPoeCaps = exports.UniFiDeviceKind = exports.UniFiRpsPortAction = exports.UniFiSmartPowerOutletAction = exports.UniFiPortOrOutletInUse = exports.UniFiSmartPowerOutletState = void 0; const pubsub_js_1 = __importDefault(require("pubsub-js")); const cache_manager_1 = require("cache-manager"); const async_lock_1 = __importDefault(require("async-lock")); const node_unifi_1 = require("node-unifi"); const cacheable_1 = require("cacheable"); var UniFiSmartPowerOutletState; (function (UniFiSmartPowerOutletState) { UniFiSmartPowerOutletState[UniFiSmartPowerOutletState["UNKNOWN"] = -1] = "UNKNOWN"; UniFiSmartPowerOutletState[UniFiSmartPowerOutletState["OFF"] = 0] = "OFF"; UniFiSmartPowerOutletState[UniFiSmartPowerOutletState["ON"] = 1] = "ON"; })(UniFiSmartPowerOutletState || (exports.UniFiSmartPowerOutletState = UniFiSmartPowerOutletState = {})); var UniFiPortOrOutletInUse; (function (UniFiPortOrOutletInUse) { UniFiPortOrOutletInUse[UniFiPortOrOutletInUse["UNKNOWN"] = -1] = "UNKNOWN"; UniFiPortOrOutletInUse[UniFiPortOrOutletInUse["NO"] = 0] = "NO"; UniFiPortOrOutletInUse[UniFiPortOrOutletInUse["YES"] = 1] = "YES"; })(UniFiPortOrOutletInUse || (exports.UniFiPortOrOutletInUse = UniFiPortOrOutletInUse = {})); var UniFiSmartPowerOutletAction; (function (UniFiSmartPowerOutletAction) { UniFiSmartPowerOutletAction[UniFiSmartPowerOutletAction["OFF"] = 0] = "OFF"; UniFiSmartPowerOutletAction[UniFiSmartPowerOutletAction["ON"] = 1] = "ON"; })(UniFiSmartPowerOutletAction || (exports.UniFiSmartPowerOutletAction = UniFiSmartPowerOutletAction = {})); var UniFiRpsPortAction; (function (UniFiRpsPortAction) { UniFiRpsPortAction[UniFiRpsPortAction["OFF"] = 0] = "OFF"; UniFiRpsPortAction[UniFiRpsPortAction["ON"] = 1] = "ON"; })(UniFiRpsPortAction || (exports.UniFiRpsPortAction = UniFiRpsPortAction = {})); var UniFiDeviceKind; (function (UniFiDeviceKind) { UniFiDeviceKind[UniFiDeviceKind["OUTLET"] = 0] = "OUTLET"; UniFiDeviceKind[UniFiDeviceKind["PORT"] = 1] = "PORT"; UniFiDeviceKind[UniFiDeviceKind["RPS_PORT"] = 2] = "RPS_PORT"; })(UniFiDeviceKind || (exports.UniFiDeviceKind = UniFiDeviceKind = {})); exports.UniFiSwitchPortPoeCaps = { '8023AF': 1, '8023AT': 2, PASV24: 4, PASSTHROUGHABLE: 8, PASSTHROUGH: 16, '8023BT': 32, }; class UniFiSmartPower { log; config; static PUB_SUB_OUTLET_TOPIC = 'outlet'; static STATUS_CACHE_KEY = 'outlet-status'; static STATUS_CACHE_TTL_MS_DEFAULT = 15 * 1000; static STATUS_CACHE_TTL_MS_MIN = 5 * 1000; static STATUS_CACHE_TTL_MS_MAX = 60 * 1000; static STATUS_POLL_INTERVAL_MS_DEFAULT = 15 * 1000; static STATUS_POLL_INTERVAL_MS_MIN = 5 * 1000; static STATUS_POLL_INTERVAL_MS_MAX = 60 * 1000; static CONTROLLER_LOCK = 'CONTROLLER_LOCK'; lock = new async_lock_1.default({ domainReentrant: true }); cache; controller; constructor(log, config) { this.log = log; this.config = config; const store = new cacheable_1.KeyvCacheableMemory({ ttl: undefined, // No default ttl lruSize: 0, // Infinite capacity }); const keyv = new cacheable_1.Keyv({ store }); this.cache = (0, cache_manager_1.createCache)({ stores: [keyv] }); this.controller = new node_unifi_1.Controller({ host: this.config.host, port: this.config.port, username: this.config.username, password: this.config.password, sslverify: false, }); } reset() { pubsub_js_1.default.clearAllSubscriptions(); } subscribe(device, kind, index, func) { const topic = UniFiSmartPower.statusTopic(device, kind, index); const token = pubsub_js_1.default.subscribe(topic, async (_, data) => { if (!data) { return; } func(data); }); this.log.debug('[API] Status subscription added for%s %s.%s [token=%s]', { [UniFiDeviceKind.OUTLET]: ' outlet', [UniFiDeviceKind.PORT]: ' port', [UniFiDeviceKind.RPS_PORT]: ' RPS port', }[kind] ?? '', device.mac, index, token); // When this is the first subscription, start polling to publish updates. if (pubsub_js_1.default.countSubscriptions(topic) === 1) { const poll = async () => { // Stop polling when there are no active subscriptions. if (pubsub_js_1.default.countSubscriptions(topic) === 0) { this.log.debug('[API] There are no status subscriptions; skipping poll'); return; } // Acquire the status lock before emitting any new events. try { switch (kind) { case UniFiDeviceKind.OUTLET: this.log.debug('[API] Polling status for outlet %s.%s', device.mac, index); pubsub_js_1.default.publish(topic, await this.getOutletStatus(device, index)); break; case UniFiDeviceKind.PORT: this.log.debug('[API] Polling status for port %s.%s', device.mac, index); pubsub_js_1.default.publish(topic, await this.getPortStatus(device, index)); break; case UniFiDeviceKind.RPS_PORT: this.log.debug('[API] Polling status for RPS port %s.%s', device.mac, index); pubsub_js_1.default.publish(topic, await this.getRpsPortStatus(device, index)); break; default: // noinspection ExceptionCaughtLocallyJS throw new Error('unknown device status kind=%d', kind); } } catch (error) { // Nothing to do here. The error has already been logged. } setTimeout(poll, this.statusPollIntervalMs); }; setTimeout(poll, 0); } return token; } unsubscribe(token) { pubsub_js_1.default.unsubscribe(token); this.log.debug('[API] Status subscription removed for token %s', token); } async getSites() { return this.lock.acquire(UniFiSmartPower.CONTROLLER_LOCK, async () => { this.log.debug('[API] Fetching sites from UniFi API'); await this.controller.login(); return (await this.controller.getSitesStats()) .filter(({ name }) => !!name) .map(({ name: id, desc: name }) => ({ id, name, })); }); } async getDeviceStatuses(siteOrDevice, acquireLock = true) { let site; let device; let errMsg = ''; if (typeof siteOrDevice === 'string') { site = siteOrDevice; errMsg = `site ${site} `; device = null; } else if (siteOrDevice) { device = siteOrDevice; site = device.site; errMsg = `device ${device.name} [${device.mac}] `; } const fetch = async () => { const result = await this.cache.wrap(UniFiSmartPower.deviceCacheKey(device), async () => { this.log.debug('[API] Fetching status from UniFi API'); this.controller.opts.site = site; let result = [], error = null; try { await this.controller.login(); result = await this.controller.getAccessDevices(device?.mac ?? ''); } catch (e) { error = e; this.log.error('[API] An error occurred polling %sfor a status update; %s', errMsg, error.message); } return (error ?? result .filter((device) => (device.outlet_table ?? []).length > 0 || (device.port_table ?? []).length > 0 || (device?.rps?.rps_port_table ?? []).length > 0) .map((device) => UniFiSmartPower.transformDeviceStatusResponse(site, device))); }, this.statusCacheTtlMs); if (result instanceof Error) { throw result; } return result; }; return acquireLock ? this.lock.acquire(UniFiSmartPower.CONTROLLER_LOCK, fetch) : fetch(); } static transformDeviceStatusResponse(site, { _id: id, ip, mac, model, version, serial: serialNumber, name, port_table: ports, port_overrides: portOverrides, outlet_table: outlets, outlet_overrides: outletOverrides, rps: rps, rps_override: rpsPortOverride, }) { return { device: { site, id, ip, mac, model, version, serialNumber, name: name || model || serialNumber, }, ports: (ports ?.filter(({ port_poe: isPoePort }) => !!isPoePort) .map((entry) => ({ index: entry.port_idx, name: entry.name ?? `Port ${entry.port_idx}`, poeMode: entry.poe_mode || 'off', inUse: !entry.poe_power ? -1 : parseFloat(entry.poe_power) > 0 ? 1 : 0, active: !!entry.poe_enable, poeOnAction: this.getPortPoeOnMode(entry.poe_caps), entry, override: portOverrides?.find((p) => p?.port_idx === entry.port_idx) ?? Object.fromEntries(Object.entries(entry).filter(([k]) => ['port_idx', 'name', 'poe_mode', 'portconf_id'].includes(k))), })) ?? []).filter((p) => p.poeOnAction !== 'off' && p.override && p.entry), outlets: (outlets?.map((entry) => ({ index: entry.index, name: entry.name ?? `Outlet ${entry.index}`, relayState: entry.relay_state ? 1 : 0, inUse: !entry.outlet_power ? -1 : parseFloat(entry.outlet_power) > 0 ? 1 : 0, entry, override: outletOverrides?.find((o) => o?.index === entry.index) ?? Object.fromEntries(Object.entries(entry).filter(([k]) => ['index', 'name', 'relay_state'].includes(k))), })) ?? []).filter((o) => o.override && o.entry), rpsPorts: (rps?.rps_port_table.map((entry) => ({ index: entry.port_idx, name: entry.name ?? `RPS Port ${entry.port_idx}`, portMode: entry.port_mode || 'disabled', inUse: entry.power_delivering ? 1 : 0, active: !!entry.power_active, entry, override: rpsPortOverride?.rps_port_table?.find((p) => p?.port_idx === entry.port_idx) ?? Object.fromEntries(Object.entries(entry).filter(([k]) => ['port_idx', 'name', 'port_mode'].includes(k))), })) ?? []).filter((p) => p.override && p.entry), }; } static getPortPoeOnMode(poeCaps) { if ( // We already filter out non-poe ports and if it doesn't have cps then it supports auto poeCaps === undefined || poeCaps === null || this.poeSupportsMode(poeCaps, exports.UniFiSwitchPortPoeCaps['8023AF']) || this.poeSupportsMode(poeCaps, exports.UniFiSwitchPortPoeCaps['8023AT']) || this.poeSupportsMode(poeCaps, exports.UniFiSwitchPortPoeCaps['8028023BT3AF'])) { return 'auto'; } if (this.poeSupportsMode(poeCaps, exports.UniFiSwitchPortPoeCaps.PASSTHROUGH) || this.poeSupportsMode(poeCaps, exports.UniFiSwitchPortPoeCaps.PASSTHROUGHABLE)) { return 'passthrough'; } if (this.poeSupportsMode(poeCaps, exports.UniFiSwitchPortPoeCaps.PASV24)) { return 'pasv24'; } return 'off'; } static poeSupportsMode(poeCaps, cap) { return !!poeCaps && (poeCaps & cap) === cap; } async getDeviceStatus(device, acquireLock = true) { const devices = await this.getDeviceStatuses(device, acquireLock); if (devices.length !== 1) { throw new Error(`unknown device with id=${device.id}`); } return devices[0]; } async getPortStatus(device, portIndex) { const { ports } = await this.getDeviceStatus(device); const portInfo = ports.find(({ index }) => index === portIndex) ?? null; if (portInfo === null) { throw new Error(`unknown port with id=${portIndex}`); } return portInfo; } async getOutletStatus(device, outletIndex) { const { outlets } = await this.getDeviceStatus(device); const outletInfo = outlets.find(({ index }) => index === outletIndex) ?? null; if (outletInfo === null) { throw new Error(`unknown outlet with id=${outletIndex}`); } return outletInfo; } async getRpsPortStatus(device, portIndex) { const { rpsPorts } = await this.getDeviceStatus(device); const portInfo = rpsPorts.find(({ index }) => index === portIndex) ?? null; if (portInfo === null) { throw new Error(`unknown rps port with id=${portIndex}`); } return portInfo; } async commandOutlet(device, outletIndex, command) { return this.lock.acquire(UniFiSmartPower.CONTROLLER_LOCK, async () => { const { outlets } = await this.getDeviceStatus(device, false); this.controller.opts.site = device.site; await this.controller.login(); await this.controller.setDeviceSettingsBase(device.id, { outlet_overrides: outlets.map((outlet) => ({ ...outlet.override, relay_state: outlet.index === outletIndex ? !!command : !!outlet.relayState, })), }); await this.cache.del(UniFiSmartPower.deviceCacheKey(device)); await this.cache.del(UniFiSmartPower.deviceCacheKey()); }); } async commandPort(device, portIndex, poeMode) { return this.lock.acquire(UniFiSmartPower.CONTROLLER_LOCK, async () => { const { ports } = await this.getDeviceStatus(device, false); this.controller.opts.site = device.site; await this.controller.login(); await this.controller.setDeviceSettingsBase(device.id, { port_overrides: ports.map((port) => ({ ...port.override, poe_mode: port.index === portIndex ? poeMode : port.poeMode, })), }); await this.cache.del(UniFiSmartPower.deviceCacheKey(device)); await this.cache.del(UniFiSmartPower.deviceCacheKey()); }); } async commandRpsPort(device, portIndex, command) { return this.lock.acquire(UniFiSmartPower.CONTROLLER_LOCK, async () => { const { rpsPorts } = await this.getDeviceStatus(device, false); this.controller.opts.site = device.site; await this.controller.login(); await this.controller.setDeviceSettingsBase(device.id, { rps_override: { rps_port_table: rpsPorts.map((port) => ({ ...port.override, port_mode: port.index === portIndex ? command === UniFiRpsPortAction.ON ? 'auto' : 'disabled' : port.portMode, })), }, }); await this.cache.del(UniFiSmartPower.deviceCacheKey(device)); await this.cache.del(UniFiSmartPower.deviceCacheKey()); }); } get statusCacheTtlMs() { return Math.max(UniFiSmartPower.STATUS_CACHE_TTL_MS_MIN, Math.min(UniFiSmartPower.STATUS_CACHE_TTL_MS_MAX, (this.config.outletStatusCacheTtl ?? 0) * 1000 || UniFiSmartPower.STATUS_CACHE_TTL_MS_DEFAULT)); } get statusPollIntervalMs() { return Math.max(UniFiSmartPower.STATUS_POLL_INTERVAL_MS_MIN, Math.min(UniFiSmartPower.STATUS_POLL_INTERVAL_MS_MAX, (this.config.outletStatusPollInterval ?? 0) * 1000 || UniFiSmartPower.STATUS_POLL_INTERVAL_MS_DEFAULT)); } static statusTopic(device, kind, index) { return `${UniFiSmartPower.PUB_SUB_OUTLET_TOPIC}.${device.id}.${kind}.${index}`; } static deviceCacheKey(device = null) { return `${UniFiSmartPower.STATUS_CACHE_KEY}.${device?.id ?? 'ALL'}`; } } exports.UniFiSmartPower = UniFiSmartPower; //# sourceMappingURL=uniFiSmartPower.js.map