UNPKG

@wanderxjtu/homebridge-yeelighter

Version:

Yeelight support for Homebridge with particular support of ceiling lights

409 lines 18.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.YeeAccessory = exports.TRACKED_ATTRIBUTES = void 0; const specs_1 = require("./specs"); const lightservice_1 = require("./lightservice"); const colorlightservice_1 = require("./colorlightservice"); const whitelightservice_1 = require("./whitelightservice"); const temperaturelightservice_1 = require("./temperaturelightservice"); const backgroundlightservice_1 = require("./backgroundlightservice"); exports.TRACKED_ATTRIBUTES = Object.keys(lightservice_1.EMPTY_ATTRIBUTES); const nameCount = new Map(); /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ class YeeAccessory { constructor(platform, device, accessory, ambientAccessory) { this.platform = platform; this.device = device; this.accessory = accessory; this.ambientAccessory = ambientAccessory; this.services = []; this._detailedLogging = false; this.displayName = "unset"; this.attributes = { ...lightservice_1.EMPTY_ATTRIBUTES }; this.lastCommandId = 1; this.queryTimestamp = 0; this.transactions = new Map(); this.debug = (message, ...optionalParameters) => { this.platform.log.debug(`[${this.name}] ${message}`, optionalParameters); }; this.log = (message, ...optionalParameters) => { this.platform.log.info(`[${this.name}] ${message}`, optionalParameters); }; this.warn = (message, ...optionalParameters) => { this.platform.log.warn(`[${this.name}] ${message}`, optionalParameters); }; this.error = (message, ...optionalParameters) => { this.platform.log.error(`[${this.name}] ${message}`, optionalParameters); }; this.getAttributes = async () => { var _a; if ((_a = this.config) === null || _a === void 0 ? void 0 : _a.blocking) { if (this.updateTimestamp < Date.now() - 1000 && (!this.updatePromise || !this.updatePromisePending)) { // make sure we don't query in parallel and not more often than every second this.updatePromise = new Promise((resolve, reject) => { this.updatePromisePending = true; this.updateResolve = resolve; this.updateReject = reject; this.requestAttributes(); }); } // this promise will be awaited for by everybody entering here while a request is still in the air if (this.updatePromise && this.connected) { const timeout = (prom, time) => { let timer; return Promise.race([ prom, new Promise((_resolve, reject) => timer = setTimeout(reject, time)) ]).finally(() => clearTimeout(timer)); }; try { await timeout(this.updatePromise, this.platform.config.timeout || 60000); } catch (error) { this.warn("retrieving attributes failed. Using last attributes.", error); } } } return this.attributes; }; this.onDeviceUpdate = (update) => { const { id, result, error } = update; if (!id) { // this is some strange unknown message this.warn("unknown response", update); return; } const transaction = this.transactions.get(id); if (!transaction) { this.warn(`no transactions found for ${id}`, update); } if (transaction) { const seconds = (Date.now() - transaction.timestamp) / 1000; this.debug(`transaction ${id} took ${seconds}s`, update); } this.transactions.delete(id); if (result && result.length === 1 && result[0] === "ok") { this.connected = true; this.debug(`received ${id}: OK`); transaction === null || transaction === void 0 ? void 0 : transaction.resolve(); // simple ok } else if (result && result.length > 3) { this.connected = true; if (this.lastCommandId !== id) { this.warn(`update with unexpected id: ${id}, expected: ${this.lastCommandId}`); } const seconds = (Date.now() - this.queryTimestamp) / 1000; this.debug(`received update ${id} after ${seconds}s: ${JSON.stringify(result)}`); if (this.updateResolve) { // resolve the promise and delete the resolvers this.updateResolve(result); this.updatePromisePending = false; delete this.updateResolve; delete this.updateReject; } const newAttributes = { ...lightservice_1.EMPTY_ATTRIBUTES }; for (const key of Object.keys(this.attributes)) { const index = exports.TRACKED_ATTRIBUTES.indexOf(key); switch (typeof lightservice_1.EMPTY_ATTRIBUTES[key]) { case "number": if (!Number.isNaN(Number(result[index]))) { newAttributes[key] = Number(result[index]); } break; case "boolean": newAttributes[key] = result[index] === "on"; break; default: newAttributes[key] = result[index]; break; } } this.updateTimestamp = Date.now(); this.onUpdateAttributes(newAttributes); transaction === null || transaction === void 0 ? void 0 : transaction.resolve(); } else if (error) { this.error(`Error returned for request [${id}]: ${JSON.stringify(error)}`); // reject any pending waits if (this.updateReject) { this.updateReject(); this.updatePromisePending = false; delete this.updateResolve; delete this.updateReject; } transaction === null || transaction === void 0 ? void 0 : transaction.reject(error); } else { this.warn(`received unhandled ${id}:`, update); transaction === null || transaction === void 0 ? void 0 : transaction.resolve(); } }; this.onUpdateAttributes = (newAttributes) => { var _a; if (JSON.stringify(this.attributes) !== JSON.stringify(newAttributes)) { if (!((_a = this.config) === null || _a === void 0 ? void 0 : _a.blocking)) { for (const service of this.services) { service.onAttributesUpdated(newAttributes); } } this.attributes = { ...newAttributes }; } }; this.onDeviceConnected = async () => { this.connected = true; this.log(`${this.info.model} Connected`); this.requestAttributes(); if (this.platform.config.interval !== 0) { this.interval = setInterval(this.onInterval, this.platform.config.interval || 60000); } }; this.onDeviceDisconnected = () => { var _a; if (this.connected) { this.connected = false; this.log("Disconnected"); if ((_a = this.config) === null || _a === void 0 ? void 0 : _a.offOnDisconnect) { this.attributes.power = false; this.attributes.bg_power = false; this.log("configured to mark as powered-off when disconnected"); for (const service of this.services) service.onPowerOff(); } if (this.updateReject) { this.updateReject(); this.updatePromisePending = false; } } if (this.interval) { clearInterval(this.interval); delete this.interval; } }; this.onDeviceError = error => { this.log("Device Error", error); }; this.onInterval = () => { if (this.connected) { const updateSince = (Date.now() - this.updateTimestamp) / 1000; const updateThreshold = ((this.platform.config.timeout || 5000) + (this.platform.config.interval || 60000)) / 1000; if (this.updateTimestamp !== 0 && updateSince > updateThreshold) { this.log(`No update received within ${updateSince}s (Threshold: ${updateThreshold} (${this.platform.config.timeout}+${this.platform.config.interval}) => switching to unreachable`); this.onDeviceDisconnected(); } else { this.requestAttributes(); } // } else { if (this.interval) { clearInterval(this.interval); delete this.interval; } } this.clearOldTransactions(); }; const deviceInfo = device.info; const support = deviceInfo.support.split(" "); let specs = specs_1.MODEL_SPECS[deviceInfo.model]; let name = deviceInfo.id; this.connected = false; const override = platform.config.override || []; if (!specs) { specs = { ...specs_1.EMPTY_SPECS }; this.warn(`no specs for light ${deviceInfo.id} ${deviceInfo.model}. It supports: ${deviceInfo.support}. Using fallback. This will not give you nightLight support.`); specs.name = deviceInfo.model; specs.color = support.includes("set_hsv"); specs.backgroundLight = support.includes("bg_set_hsv"); specs.nightLight = false; if (!support.includes("set_ct_abx")) { specs.colorTemperature.min = 0; specs.colorTemperature.max = 0; } } const overrideConfig = override === null || override === void 0 ? void 0 : override.find(item => item.id === deviceInfo.id); if (overrideConfig === null || overrideConfig === void 0 ? void 0 : overrideConfig.backgroundLight) { specs.backgroundLight = overrideConfig.backgroundLight; } if (overrideConfig === null || overrideConfig === void 0 ? void 0 : overrideConfig.color) { specs.color = overrideConfig.color; } if (overrideConfig === null || overrideConfig === void 0 ? void 0 : overrideConfig.name) { name = overrideConfig.name; } this.name = name; if (overrideConfig === null || overrideConfig === void 0 ? void 0 : overrideConfig.nightLight) { specs.nightLight = overrideConfig.nightLight; } this.specs = specs; this._detailedLogging = !!(overrideConfig === null || overrideConfig === void 0 ? void 0 : overrideConfig.log); this.connectDevice(this.device); let typeString = "UNKNOWN"; const parameters = { accessory, platform, light: this, }; if (specs.color) { this.services.push(new colorlightservice_1.ColorLightService(parameters)); typeString = "Color light"; } else { if (specs.colorTemperature.min === 0 && specs.colorTemperature.max === 0) { this.services.push(new whitelightservice_1.WhiteLightService(parameters)); typeString = "White light"; } else { this.services.push(new temperaturelightservice_1.TemperatureLightService(parameters)); typeString = "Color temperature light"; } } if (support.includes("bg_set_power")) { if (this.config.separateAmbient && ambientAccessory) { this.services.push(new backgroundlightservice_1.BackgroundLightService({ ...parameters, accessory: ambientAccessory })); } else { this.services.push(new backgroundlightservice_1.BackgroundLightService(parameters)); } typeString = `${typeString} with ambience light`; } this.support = support; this.updateTimestamp = 0; this.updatePromisePending = false; this.setInfoService(overrideConfig, accessory); if (ambientAccessory) { this.setInfoService(overrideConfig, ambientAccessory); } // name handling this.log(`installed as ${typeString}`); } static instance(device, platform, accessory, ambientAccessory) { const cache = YeeAccessory.handledAccessories.get(device.info.id); if (cache) { return cache; } const a = new YeeAccessory(platform, device, accessory, ambientAccessory); YeeAccessory.handledAccessories.set(device.info.id, a); return a; } get detailedLogging() { return this._detailedLogging; } get info() { return this.device.info; } get config() { const override = (this.platform.config.override || []); const { device } = this.accessory.context; const overrideConfig = override.find(item => item.id === device.id); return overrideConfig || { id: device.id }; } setAttributes(attributes) { this.attributes = { ...this.attributes, ...attributes }; } connectDevice(device) { device.connect(); device.on("deviceUpdate", this.onDeviceUpdate); device.on("connected", this.onDeviceConnected); device.on("disconnected", this.onDeviceDisconnected); device.on("deviceError", this.onDeviceError); } // Respond to identify request identify(callback) { this.log(`Hi ${this.info.model}`); callback(); } setNameService(service) { service.getCharacteristic(this.platform.Characteristic.ConfiguredName).on("set", (value, callback) => { this.log(`setting name to "${value}".`); service.displayName = value.toString(); this.displayName = value.toString(); service.setCharacteristic(this.platform.Characteristic.Name, value); for (const service of this.services) { service.updateName(value.toString()); } this.platform.api.updatePlatformAccessories([this.accessory]); callback(); }); } setInfoService(override, accessory) { const { platform, specs, info } = this; // set accessory information let infoService = accessory.getService(platform.Service.AccessoryInformation); let name = (override === null || override === void 0 ? void 0 : override.name) || specs.name; let count = nameCount.get(name) || 0; count = count + 1; nameCount.set(name, count); if (count > 1) { name = `${name} ${count}`; } if (!infoService) { infoService = new platform.Service.AccessoryInformation(); infoService .updateCharacteristic(platform.Characteristic.Manufacturer, "Yeelighter") .updateCharacteristic(platform.Characteristic.Model, specs.name) .updateCharacteristic(platform.Characteristic.Name, name) .updateCharacteristic(platform.Characteristic.SerialNumber, info.id) .updateCharacteristic(platform.Characteristic.FirmwareRevision, info.fw_ver); accessory.addService(infoService); } else { // re-use service from cache infoService .updateCharacteristic(platform.Characteristic.Manufacturer, "Yeelighter") .updateCharacteristic(platform.Characteristic.Model, specs.name) .updateCharacteristic(platform.Characteristic.Name, name) .updateCharacteristic(platform.Characteristic.SerialNumber, info.id) .updateCharacteristic(platform.Characteristic.FirmwareRevision, info.fw_ver); } this.setNameService(infoService); return infoService; } sendCommand(method, parameters) { if (!this.connected) { this.warn("send command but device doesn't seem connected"); } const supportedCommands = this.device.info.support.split(","); if (!supportedCommands.includes) { this.warn(`sending ${method} although unsupported.`); } const id = this.lastCommandId + 1; this.debug(`sendCommand(${id}, ${method}, ${JSON.stringify(parameters)})`); this.device.sendCommand({ id, method, params: parameters }); this.lastCommandId = id; return id; } async sendCommandPromise(method, parameters) { return new Promise((resolve, reject) => { const timestamp = Date.now(); const id = this.sendCommand(method, parameters); this.debug(`sent command ${id}: ${method}`, parameters); this.transactions.set(id, { resolve, reject, timestamp }); }); } clearOldTransactions() { for (const [key, item] of this.transactions.entries()) { // clear transactions older than 60s if (item.timestamp > Date.now() + 60000) { this.log(`error: timeout for request ${key}`); item.reject(new Error("timeout")); this.transactions.delete(key); } } } async requestAttributes() { this.queryTimestamp = Date.now(); // do not await. We handle the resolve/reject in onDeviceUpdate this.sendCommandPromise("get_prop", this.device.info.trackedAttributes); this.debug(`requesting attributes. Transactions: ${this.transactions.size}`); } } exports.YeeAccessory = YeeAccessory; YeeAccessory.handledAccessories = new Map(); //# sourceMappingURL=yeeaccessory.js.map