homebridge-yeelighter
Version:
Yeelight support for Homebridge with particular support of ceiling lights
329 lines • 12.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LightService = exports.EMPTY_ATTRIBUTES = exports.POWERMODE_MOON = exports.POWERMODE_HSV = exports.POWERMODE_CT = exports.POWERMODE_DEFAULT = void 0;
exports.powerModeFromColorModeAndActiveMode = powerModeFromColorModeAndActiveMode;
exports.convertColorTemperature = convertColorTemperature;
exports.isValidValue = isValidValue;
const colortools_1 = require("./colortools");
exports.POWERMODE_DEFAULT = 0;
exports.POWERMODE_CT = 1;
exports.POWERMODE_HSV = 3;
exports.POWERMODE_MOON = 5;
exports.EMPTY_ATTRIBUTES = {
power: false,
color_mode: 0,
bright: 0,
hue: 0,
sat: 0,
ct: 0,
bg_power: false,
bg_bright: 0,
bg_hue: 0,
bg_sat: 0,
bg_ct: 0,
bg_lmode: 0,
nl_br: 0,
active_mode: 0,
name: "unknown"
};
function powerModeFromColorModeAndActiveMode(color_mode, active_mode) {
// PowerMode:
// 0: Normal turn on operation(default value)
// 1: Turn on and switch to CT mode. (used for white lights)
// 2: Turn on and switch to RGB mode. (never used here)
// 3: Turn on and switch to HSV mode. (used for color lights)
// 4: Turn on and switch to color flow mode.
// 5: Turn on and switch to Night light mode. (Ceiling light only).
// ColorMode:
// 1 means color mode, (rgb -- never used here)
// 2 means color temperature mode, (CT used for white light)
// 3 means HSV mode (used for color lights)
if (active_mode === 1) {
return exports.POWERMODE_MOON;
}
switch (color_mode) {
case 1: {
// this is never used
return exports.POWERMODE_DEFAULT;
}
case 2: {
return exports.POWERMODE_CT;
}
case 3: {
return exports.POWERMODE_HSV;
}
default: {
// this should never happen!
return exports.POWERMODE_DEFAULT;
}
}
}
/**
* Converts a color temperature in Kelvin to a mired value for Homebridge.
* The mired value is the reciprocal of the color temperature in microreciprocal degrees.
* The formula is: mired = 1,000,000 / kelvin.
* Therefore it can also be used for reversed: kelvin = 1,000,000 / mired.
*
* @param kelvin - The color temperature in Kelvin (e.g., 2000K to 6500K).
* @returns The corresponding value in mireds (HomeKit-compatible scale).
*/
function convertColorTemperature(kelvin) {
// check if value is valid
if (Number.isFinite(kelvin) && kelvin > 0) {
return Math.min(Math.round(1000000 / kelvin), 370);
}
// This will cause a warning in the logs
return 1000;
}
function isValidValue(value) {
switch (typeof value) {
case "boolean": {
return true;
}
case "number": {
return Number.isFinite(value);
}
case "string": {
return true;
}
default: {
return false;
}
}
}
/**
* Base class for services representing a light. This is a mixin class that is
* used by the concrete light services.
*/
class LightService {
constructor(parameters, subtype) {
var _a;
this.subtype = subtype;
this.debounceTimers = {};
this.log = (message, ...optionalParameters) => {
this.platform.log.info(`${this.logPrefix} ${message}`, optionalParameters);
};
this.warn = (message, ...optionalParameters) => {
this.platform.log.warn(`${this.logPrefix} ${message}`, optionalParameters);
};
this.error = (message, ...optionalParameters) => {
this.platform.log.error(`${this.logPrefix} ${message}`, optionalParameters);
};
this.debug = (message, ...optionalParameters) => {
if (this.light.detailedLogging) {
this.platform.log.info(`${this.logPrefix} ${message}`, optionalParameters);
}
else {
this.platform.log.debug(`${this.logPrefix} ${message}`, optionalParameters);
}
};
this.onPowerOff = () => {
this.updateCharacteristic(this.platform.Characteristic.On, false);
};
this.platform = parameters.platform;
this.accessory = parameters.accessory;
this.light = parameters.light;
// we use powerMode to store the currently set mode
switch (this.light.info.color_mode) {
case 2: {
this.powerMode = exports.POWERMODE_CT;
break;
}
case 3: {
this.powerMode = exports.POWERMODE_HSV;
break;
}
default: {
// this should never happen!
this.powerMode = exports.POWERMODE_DEFAULT;
break;
}
}
this.name = ((_a = this.config) === null || _a === void 0 ? void 0 : _a.name) || this.device.info.id;
// get the LightBulb service if it exists, otherwise create a new LightBulb service
// we create multiple services for lights that have a subtype set
if (subtype) {
const subtypeUid = `${this.light.info.id}#${subtype}`;
this.log(`registering subtype ${subtypeUid}`);
this.service =
this.accessory.getService(subtypeUid) ||
this.accessory.addService(this.platform.Service.Lightbulb, `${this.name} ${subtype}`, subtypeUid);
}
else {
this.log(`no subtype`);
this.service =
this.accessory.getService(this.platform.Service.Lightbulb) ||
this.accessory.addService(this.platform.Service.Lightbulb);
}
// name handling
this.service.getCharacteristic(this.platform.Characteristic.ConfiguredName).on("set", (value, callback) => {
this.log("setConfiguredName", value);
const name = value.toString();
this.service.displayName = name;
this.name = name;
this.service.setCharacteristic(this.platform.Characteristic.Name, value);
this.platform.api.updatePlatformAccessories([this.accessory]);
callback();
});
}
cancelAllDebounces() {
for (const key in this.debounceTimers) {
clearTimeout(this.debounceTimers[key]);
delete this.debounceTimers[key];
}
}
get detailedLogging() {
return this.light.detailedLogging;
}
updateName(value) {
this.debug("Ignoring updateName", value);
// this.name = value;
// this.service.setCharacteristic(this.platform.Characteristic.Name, `${value} ${this.subtype}`);
// this.platform.api.updatePlatformAccessories([this.accessory]);
}
get device() {
return this.light.device;
}
get logPrefix() {
if (this.subtype) {
return `[${this.name}#${this.subtype}]`;
}
return `[${this.name}]`;
}
get config() {
const override = (this.platform.config.override || []);
const { info } = this.device;
const overrideConfig = override.find((item) => item.id === info.id);
return overrideConfig || { id: info.id };
}
get specs() {
return this.light.specs;
}
async attributes() {
return this.light.getAttributes();
}
async getAttribute(attribute) {
// this should never throw
const a = await this.attributes();
return a[attribute];
}
setAttributes(attributes) {
this.light.setAttributes(attributes);
}
handleCharacteristic(uuid, getter, setter) {
const characteristic = this.service.getCharacteristic(uuid);
if (!characteristic) {
throw new Error("Could not get Characteristic");
}
characteristic.on("get", async (callback) => {
if (this.light.connected) {
callback(undefined, await getter());
}
else {
callback(new Error("light disconnected"));
}
});
characteristic.on("set", async (value, callback) => {
if (this.light.connected && isValidValue(value)) {
await setter(value);
callback();
}
else {
this.log(`failed to set to value`, value, this.light.connected);
callback(new Error("light disconnected or invalid value"));
}
});
return characteristic;
}
async updateCharacteristic(uuid, value) {
const characteristic = this.service.getCharacteristic(uuid);
if (!characteristic) {
throw undefined;
}
if (isValidValue(value)) {
characteristic.updateValue(value);
}
else {
this.error("updateCharacteristic value is not finite", value);
}
}
async sendCommandPromiseWithErrorHandling(method, parameters) {
try {
await this.light.sendCommandPromise(method, parameters);
}
catch (error) {
// catch all errors so Homebridge doesn't crash
this.warn("Command failed", error);
}
}
async sendCommand(method, parameters) {
return this.sendCommandPromiseWithErrorHandling(method, parameters);
}
async sendSuddenCommand(method, parameter) {
return this.sendCommandPromiseWithErrorHandling(method, [parameter, "sudden", 0]);
}
async sendSmoothCommand(method, parameter) {
return this.sendCommandPromiseWithErrorHandling(method, [parameter, "smooth", 500]);
}
async sendAnimatedCommand(method, parameters) {
var _a, _b, _c;
const messageParameters = Array.isArray(parameters) ? parameters : [parameters];
if ((_a = this.platform.config) === null || _a === void 0 ? void 0 : _a.animateChanges) {
const animationTime = (_c = (_b = this.platform.config) === null || _b === void 0 ? void 0 : _b.animationTime) !== null && _c !== void 0 ? _c : 500;
if (this.debounceTimers[method]) {
clearTimeout(this.debounceTimers[method]);
}
this.debounceTimers[method] = setTimeout(async () => {
await this.sendCommandPromiseWithErrorHandling(method, [...messageParameters, "smooth", animationTime]);
delete this.debounceTimers[method];
}, animationTime);
return;
}
else {
return this.sendCommandPromiseWithErrorHandling(method, [...messageParameters, "sudden", 0]);
}
}
saveDefaultIfNeeded() {
var _a;
if ((_a = this.config) === null || _a === void 0 ? void 0 : _a.saveDefault) {
this.sendCommand("set_default", []);
}
}
async ensurePowerMode(mode, prefix = "") {
if (this.powerMode !== mode) {
await this.sendCommand(`${prefix}set_power`, ["on", "sudden", 0, mode]);
this.powerMode = mode;
if (prefix == "bg_") {
this.setAttributes({ bg_power: true });
}
else {
this.setAttributes({ power: true, active_mode: mode == exports.POWERMODE_MOON ? 1 : 0 });
}
}
}
async setHSV(prefix = "") {
const hue = this.lastHue;
const sat = this.lastSat;
if (hue && sat) {
await this.ensurePowerMode(exports.POWERMODE_HSV, prefix);
const hsv = [hue, sat];
delete this.lastHue;
delete this.lastSat;
await this.sendAnimatedCommand(`${prefix}set_hsv`, hsv);
if (prefix == "bg_") {
this.setAttributes({ bg_hue: hue, bg_sat: sat });
}
else {
this.setAttributes({ hue, sat });
}
this.saveDefaultIfNeeded();
}
}
updateColorFromCT(value) {
const { h, s } = (0, colortools_1.convertHomeKitColorTemperatureToHomeKitColor)(value);
this.service.getCharacteristic(this.platform.Characteristic.Hue).updateValue(h);
this.service.getCharacteristic(this.platform.Characteristic.Saturation).updateValue(s);
}
}
exports.LightService = LightService;
//# sourceMappingURL=lightservice.js.map