@wanderxjtu/homebridge-yeelighter
Version:
Yeelight support for Homebridge with particular support of ceiling lights
409 lines • 18.8 kB
JavaScript
"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