homebridge-tsvesync
Version:
Homebridge plugin for VeSync devices including Levoit air purifiers, humidifiers, and Etekcity smart outlets
623 lines • 26.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LightAccessory = void 0;
const base_accessory_1 = require("./base.accessory");
// Constants for color temperature
const MIN_COLOR_TEMP = 140; // 7143K (cool)
const MAX_COLOR_TEMP = 500; // 2000K (warm)
const DEFAULT_COLOR_TEMP = MIN_COLOR_TEMP;
const DEVICE_MIN_KELVIN = 2700;
const DEVICE_MAX_KELVIN = 6500;
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
const clampMired = (value) => clamp(Math.round(value), MIN_COLOR_TEMP, MAX_COLOR_TEMP);
const percentToKelvin = (percent) => DEVICE_MIN_KELVIN + ((DEVICE_MAX_KELVIN - DEVICE_MIN_KELVIN) * clamp(percent, 0, 100) / 100);
const kelvinToPercent = (kelvin) => ((clamp(kelvin, DEVICE_MIN_KELVIN, DEVICE_MAX_KELVIN) - DEVICE_MIN_KELVIN) / (DEVICE_MAX_KELVIN - DEVICE_MIN_KELVIN)) * 100;
const kelvinToMired = (kelvin) => Math.round(1000000 / clamp(kelvin, DEVICE_MIN_KELVIN, DEVICE_MAX_KELVIN));
const miredToKelvin = (mired) => clamp(Math.round(1000000 / clamp(mired, MIN_COLOR_TEMP, MAX_COLOR_TEMP)), DEVICE_MIN_KELVIN, DEVICE_MAX_KELVIN);
const percentToMired = (percent) => clampMired(kelvinToMired(percentToKelvin(percent)));
const miredToPercent = (mired) => kelvinToPercent(miredToKelvin(mired));
class LightAccessory extends base_accessory_1.BaseAccessory {
constructor(platform, accessory, device) {
super(platform, accessory, device);
this.isDimmerDevice = false;
this.indicatorColorState = { hue: 0, saturation: 0 };
this.lastKnownDimmerBrightness = 100;
this.lastDimmerRefresh = 0;
this.device = device;
const initialBrightness = Number(device === null || device === void 0 ? void 0 : device.brightness);
if (!Number.isNaN(initialBrightness) && initialBrightness > 0) {
this.lastKnownDimmerBrightness = Math.min(100, Math.max(1, initialBrightness));
}
}
setupService() {
// Get or create the light service
this.service = this.accessory.getService(this.platform.Service.Lightbulb) ||
this.accessory.addService(this.platform.Service.Lightbulb);
// Set up required characteristics
this.setupCharacteristic(this.platform.Characteristic.On, this.getOn.bind(this), this.setOn.bind(this));
// Set up optional characteristics based on device capabilities
this.isDimmerDevice = this.detectDimmer(this.device);
this.capabilities = this.getDeviceCapabilities();
const capabilities = this.capabilities;
if (capabilities.hasBrightness) {
this.setupCharacteristic(this.platform.Characteristic.Brightness, this.getBrightness.bind(this), this.setBrightness.bind(this));
}
if (capabilities.hasColorTemp) {
this.setupColorTemperature();
}
if (capabilities.hasColor) {
this.setupColor();
}
// Add Name characteristic
this.setupCharacteristic(this.platform.Characteristic.Name, async () => this.device.deviceName);
if (this.isDimmerDevice && this.supportsIndicatorLight()) {
this.setupIndicatorService();
}
}
setupColorTemperature() {
this.service.getCharacteristic(this.platform.Characteristic.ColorTemperature)
.setProps({
minValue: MIN_COLOR_TEMP,
maxValue: MAX_COLOR_TEMP,
});
this.setupCharacteristic(this.platform.Characteristic.ColorTemperature, this.getColorTemperature.bind(this), this.setColorTemperature.bind(this));
}
setupColor() {
this.setupCharacteristic(this.platform.Characteristic.Hue, this.getHue.bind(this), this.setHue.bind(this));
this.setupCharacteristic(this.platform.Characteristic.Saturation, this.getSaturation.bind(this), this.setSaturation.bind(this));
}
/**
* Update device states based on the latest details
*/
async updateDeviceSpecificStates(details) {
var _a;
const lightDetails = details;
const deviceStatus = (_a = lightDetails.deviceStatus) !== null && _a !== void 0 ? _a : this.device.deviceStatus;
const isActive = deviceStatus === 'on';
this.updateCharacteristicValue(this.platform.Characteristic.On, isActive);
// Update brightness if supported
if (this.capabilities.hasBrightness) {
let brightness = lightDetails.brightness;
if (!this.isDimmerDevice) {
const bulb = this.device;
if (typeof bulb.getBrightness === 'function') {
brightness = bulb.getBrightness();
}
}
if (brightness !== undefined) {
this.updateCharacteristicValue(this.platform.Characteristic.Brightness, brightness);
if (this.isDimmerDevice && brightness > 0) {
this.lastKnownDimmerBrightness = brightness;
}
}
}
// Update color temperature if supported
if (this.capabilities.hasColorTemp && !this.isDimmerDevice) {
const bulb = this.device;
let tempPercent;
if (typeof bulb.getColorTempPercent === 'function') {
tempPercent = bulb.getColorTempPercent();
}
else if (typeof lightDetails.colorTemp === 'number') {
tempPercent = lightDetails.colorTemp > 100
? miredToPercent(lightDetails.colorTemp)
: lightDetails.colorTemp;
}
if (typeof tempPercent === 'number' && !Number.isNaN(tempPercent) && tempPercent > 0) {
const mired = percentToMired(tempPercent);
this.updateCharacteristicValue(this.platform.Characteristic.ColorTemperature, mired);
}
}
// Update color if supported
if (this.capabilities.hasColor && !this.isDimmerDevice) {
const bulb = this.device;
const colorModel = typeof bulb.getColorModel === 'function' ? bulb.getColorModel() : 'none';
if (colorModel === 'rgb' && typeof bulb.getRGBValues === 'function') {
const rgb = bulb.getRGBValues();
const hsv = this.rgbToHsv(rgb.red, rgb.green, rgb.blue);
this.updateCharacteristicValue(this.platform.Characteristic.Hue, hsv.hue);
this.updateCharacteristicValue(this.platform.Characteristic.Saturation, hsv.saturation);
}
else {
const hue = typeof bulb.getColorHue === 'function'
? bulb.getColorHue()
: lightDetails.hue;
const saturation = typeof bulb.getColorSaturation === 'function'
? bulb.getColorSaturation()
: lightDetails.saturation;
if (typeof hue === 'number') {
this.updateCharacteristicValue(this.platform.Characteristic.Hue, hue);
}
if (typeof saturation === 'number') {
this.updateCharacteristicValue(this.platform.Characteristic.Saturation, saturation);
}
}
}
if (this.isDimmerDevice && this.indicatorService) {
const dimmer = lightDetails;
const isRgbOn = dimmer.rgbLightStatus === 'on';
this.updateIndicatorCharacteristic(this.platform.Characteristic.On, isRgbOn);
const { hue, saturation } = this.rgbToHsv(dimmer.rgbLightValue.red, dimmer.rgbLightValue.green, dimmer.rgbLightValue.blue);
this.indicatorColorState = { hue, saturation };
this.updateIndicatorCharacteristic(this.platform.Characteristic.Hue, hue);
this.updateIndicatorCharacteristic(this.platform.Characteristic.Saturation, saturation);
}
}
getDeviceCapabilities() {
if (this.isDimmerDevice) {
return {
hasBrightness: true,
hasColorTemp: false,
hasColor: false,
hasSpeed: false,
hasHumidity: false,
hasAirQuality: false,
hasWaterLevel: false,
hasChildLock: false,
hasSwingMode: false,
};
}
const model = this.device.deviceType.toUpperCase();
const bulb = this.device;
const supports = typeof bulb.hasFeature === 'function'
? bulb.hasFeature.bind(bulb)
: (feature) => {
if (feature === 'color_temp') {
return model.includes('CW') || model.includes('MC') || model === 'XYD0001';
}
if (feature === 'rgb_shift') {
return model.includes('MC') || model === 'XYD0001';
}
if (feature === 'dimmable') {
return true;
}
return false;
};
return {
hasBrightness: typeof this.device.setBrightness === 'function',
hasColorTemp: supports('color_temp'),
hasColor: supports('rgb_shift'),
hasSpeed: false,
hasHumidity: false,
hasAirQuality: false,
hasWaterLevel: false,
hasChildLock: false,
hasSwingMode: false,
};
}
async getOn() {
return this.device.deviceStatus === 'on';
}
async setOn(value) {
try {
const isOn = value;
if (this.isDimmerDevice) {
const dimmer = this.device;
if (isOn) {
const toggled = await dimmer.turnOn();
if (!toggled) {
throw new Error('Failed to turn on device');
}
const targetBrightness = this.resolveDimmerOnBrightness();
if (targetBrightness > 0) {
const brightnessSet = await dimmer.setBrightness(targetBrightness);
if (!brightnessSet) {
throw new Error(`Failed to restore brightness to ${targetBrightness}`);
}
this.lastKnownDimmerBrightness = targetBrightness;
this.updateCharacteristicValue(this.platform.Characteristic.Brightness, targetBrightness);
await this.persistDeviceState('brightness', targetBrightness);
}
}
else {
const toggled = await dimmer.turnOff();
if (!toggled) {
throw new Error('Failed to turn off device');
}
this.updateCharacteristicValue(this.platform.Characteristic.Brightness, 0);
await this.persistDeviceState('brightness', 0);
}
await this.persistDeviceState('deviceStatus', isOn ? 'on' : 'off');
await this.refreshDimmerDetails();
return;
}
const success = isOn ? await this.device.turnOn() : await this.device.turnOff();
if (!success) {
throw new Error(`Failed to turn ${isOn ? 'on' : 'off'} device`);
}
await this.persistDeviceState('deviceStatus', isOn ? 'on' : 'off');
}
catch (error) {
this.handleDeviceError('set on state', error);
}
}
async getBrightness() {
const deviceWithGetter = this.device;
if (typeof deviceWithGetter.getBrightness === 'function') {
return deviceWithGetter.getBrightness();
}
return this.device.brightness;
}
async setBrightness(value) {
try {
const target = Math.round(Number(value));
const brightness = Math.min(100, Math.max(this.isDimmerDevice ? 0 : 0, target));
let success = await this.device.setBrightness(brightness);
if (!success && this.isDimmerDevice && brightness === 0) {
const fallback = await this.device.turnOff();
if (!fallback) {
throw new Error('Failed to turn off device when setting brightness to 0');
}
success = true;
}
if (!success) {
throw new Error(`Failed to set brightness to ${brightness}`);
}
await this.persistDeviceState('brightness', brightness);
if (this.isDimmerDevice) {
if (brightness > 0) {
this.lastKnownDimmerBrightness = brightness;
await this.persistDeviceState('deviceStatus', 'on');
}
else {
await this.persistDeviceState('deviceStatus', 'off');
}
await this.refreshDimmerDetails();
}
}
catch (error) {
this.handleDeviceError('set brightness', error);
}
}
async getColorTemperature() {
var _a, _b, _c;
if (this.isDimmerDevice) {
return DEFAULT_COLOR_TEMP;
}
const bulb = this.device;
if (typeof bulb.getColorTempPercent === 'function') {
const percent = bulb.getColorTempPercent();
if (percent > 0) {
return percentToMired(percent);
}
}
const stored = (_c = (_b = (_a = this.accessory.context) === null || _a === void 0 ? void 0 : _a.device) === null || _b === void 0 ? void 0 : _b.details) === null || _c === void 0 ? void 0 : _c.colorTemp;
if (typeof stored === 'number') {
return clampMired(stored);
}
return DEFAULT_COLOR_TEMP;
}
async setColorTemperature(value) {
try {
if (this.isDimmerDevice) {
throw new Error('Device does not support color temperature');
}
const bulb = this.device;
if (!bulb.setColorTemperature) {
throw new Error('Device does not support color temperature');
}
const percent = miredToPercent(Number(value));
const success = await bulb.setColorTemperature(percent);
if (!success) {
throw new Error(`Failed to set color temperature to ${value}`);
}
await this.persistDeviceState('colorTemp', value);
}
catch (error) {
this.handleDeviceError('set color temperature', error);
}
}
async getHue() {
if (this.isDimmerDevice) {
return 0;
}
const bulb = this.device;
const colorModel = typeof bulb.getColorModel === 'function' ? bulb.getColorModel() : 'none';
if (colorModel === 'rgb' && typeof bulb.getRGBValues === 'function') {
const rgb = bulb.getRGBValues();
return this.rgbToHsv(rgb.red, rgb.green, rgb.blue).hue;
}
if (typeof bulb.getColorHue === 'function') {
return bulb.getColorHue();
}
return 0;
}
async getSaturation() {
if (this.isDimmerDevice) {
return 0;
}
const bulb = this.device;
const colorModel = typeof bulb.getColorModel === 'function' ? bulb.getColorModel() : 'none';
if (colorModel === 'rgb' && typeof bulb.getRGBValues === 'function') {
const rgb = bulb.getRGBValues();
return this.rgbToHsv(rgb.red, rgb.green, rgb.blue).saturation;
}
if (typeof bulb.getColorSaturation === 'function') {
return bulb.getColorSaturation();
}
return 0;
}
async setHue(value) {
try {
if (this.isDimmerDevice) {
throw new Error('Device does not support color');
}
const bulb = this.device;
if (!bulb.setColor) {
throw new Error('Device does not support color');
}
const fallbackSaturation = this.service.getCharacteristic(this.platform.Characteristic.Saturation).value;
const currentSaturation = typeof bulb.getColorSaturation === 'function'
? bulb.getColorSaturation()
: (typeof fallbackSaturation === 'number' ? fallbackSaturation : 0);
const currentValue = typeof bulb.getColorValue === 'function'
? bulb.getColorValue()
: 100;
const success = await bulb.setColor(Number(value), currentSaturation || 0, currentValue || 100);
if (!success) {
throw new Error(`Failed to set hue to ${value}`);
}
await this.persistDeviceState('hue', value);
}
catch (error) {
this.handleDeviceError('set hue', error);
}
}
async setSaturation(value) {
try {
if (this.isDimmerDevice) {
throw new Error('Device does not support color');
}
const bulb = this.device;
if (!bulb.setColor) {
throw new Error('Device does not support color');
}
const fallbackHue = this.service.getCharacteristic(this.platform.Characteristic.Hue).value;
const currentHue = typeof bulb.getColorHue === 'function'
? bulb.getColorHue()
: (typeof fallbackHue === 'number' ? fallbackHue : 0);
const currentValue = typeof bulb.getColorValue === 'function'
? bulb.getColorValue()
: 100;
const success = await bulb.setColor(currentHue || 0, Number(value), currentValue || 100);
if (!success) {
throw new Error(`Failed to set saturation to ${value}`);
}
await this.persistDeviceState('saturation', value);
}
catch (error) {
this.handleDeviceError('set saturation', error);
}
}
detectDimmer(device) {
return typeof device.rgbColorSet === 'function';
}
supportsIndicatorLight() {
const model = this.device.deviceType.toUpperCase();
if (model.startsWith('ESWD16')) {
return false;
}
return typeof this.device.rgbColorSet === 'function';
}
setupIndicatorService() {
const serviceName = `${this.device.deviceName} Indicator`;
const existing = this.accessory.getService(serviceName);
this.indicatorService = existing || this.accessory.addService(this.platform.Service.Lightbulb, serviceName, 'indicator');
const indicatorService = this.indicatorService;
if (!indicatorService) {
return;
}
this.setupCharacteristicForService(indicatorService, this.platform.Characteristic.On, this.getIndicatorOn.bind(this), this.setIndicatorOn.bind(this));
this.setupCharacteristicForService(indicatorService, this.platform.Characteristic.Hue, this.getIndicatorHue.bind(this), this.setIndicatorHue.bind(this));
this.setupCharacteristicForService(indicatorService, this.platform.Characteristic.Saturation, this.getIndicatorSaturation.bind(this), this.setIndicatorSaturation.bind(this));
}
setupCharacteristicForService(service, characteristic, onGet, onSet) {
this.withService(service, () => {
this.setupCharacteristic(characteristic, onGet, onSet);
});
}
updateIndicatorCharacteristic(characteristic, value) {
if (!this.indicatorService) {
return;
}
this.withService(this.indicatorService, () => {
this.updateCharacteristicValue(characteristic, value);
});
}
withService(service, fn) {
const currentService = this.service;
this.service = service;
try {
return fn();
}
finally {
this.service = currentService;
}
}
async getIndicatorOn() {
if (!this.isDimmerDevice) {
return false;
}
const dimmer = this.device;
return dimmer.rgbLightStatus === 'on';
}
async setIndicatorOn(value) {
if (!this.isDimmerDevice) {
return;
}
const isOn = Boolean(value);
const dimmer = this.device;
const success = isOn ? await dimmer.rgbColorOn() : await dimmer.rgbColorOff();
if (!success) {
throw new Error(`Failed to turn ${isOn ? 'on' : 'off'} indicator light`);
}
await this.refreshDimmerDetails();
}
async getIndicatorHue() {
if (!this.isDimmerDevice) {
return 0;
}
const { hue } = this.rgbToHsv(this.device.rgbLightValue.red, this.device.rgbLightValue.green, this.device.rgbLightValue.blue);
this.indicatorColorState.hue = hue;
return hue;
}
async setIndicatorHue(value) {
if (!this.isDimmerDevice) {
return;
}
this.indicatorColorState.hue = Number(value);
this.scheduleIndicatorColorUpdate();
}
async getIndicatorSaturation() {
if (!this.isDimmerDevice) {
return 0;
}
const { saturation } = this.rgbToHsv(this.device.rgbLightValue.red, this.device.rgbLightValue.green, this.device.rgbLightValue.blue);
this.indicatorColorState.saturation = saturation;
return saturation;
}
async setIndicatorSaturation(value) {
if (!this.isDimmerDevice) {
return;
}
this.indicatorColorState.saturation = Number(value);
this.scheduleIndicatorColorUpdate();
}
scheduleIndicatorColorUpdate() {
if (this.indicatorColorUpdateTimeout) {
clearTimeout(this.indicatorColorUpdateTimeout);
}
this.indicatorColorUpdateTimeout = setTimeout(() => {
void this.pushIndicatorColor().catch(error => {
void this.handleDeviceError('set indicator color', error);
});
}, 250);
}
async pushIndicatorColor() {
if (!this.isDimmerDevice) {
return;
}
const [red, green, blue] = this.hsvToRgb(this.indicatorColorState.hue, this.indicatorColorState.saturation, 100);
const dimmer = this.device;
if (dimmer.rgbLightStatus !== 'on') {
await dimmer.rgbColorOn();
}
const success = await dimmer.rgbColorSet(red, green, blue);
if (!success) {
throw new Error('Failed to update indicator color');
}
this.indicatorColorState = {
hue: this.indicatorColorState.hue,
saturation: this.indicatorColorState.saturation,
};
await this.refreshDimmerDetails();
}
hsvToRgb(h, s, v) {
let hue = h % 360;
if (hue < 0) {
hue += 360;
}
const saturation = Math.max(0, Math.min(100, s)) / 100;
const value = Math.max(0, Math.min(100, v)) / 100;
const c = value * saturation;
const x = c * (1 - Math.abs(((hue / 60) % 2) - 1));
const m = value - c;
let rPrime = 0;
let gPrime = 0;
let bPrime = 0;
if (hue < 60) {
rPrime = c;
gPrime = x;
}
else if (hue < 120) {
rPrime = x;
gPrime = c;
}
else if (hue < 180) {
gPrime = c;
bPrime = x;
}
else if (hue < 240) {
gPrime = x;
bPrime = c;
}
else if (hue < 300) {
rPrime = x;
bPrime = c;
}
else {
rPrime = c;
bPrime = x;
}
const red = Math.round((rPrime + m) * 255);
const green = Math.round((gPrime + m) * 255);
const blue = Math.round((bPrime + m) * 255);
return [red, green, blue];
}
rgbToHsv(r, g, b) {
const red = r / 255;
const green = g / 255;
const blue = b / 255;
const max = Math.max(red, green, blue);
const min = Math.min(red, green, blue);
const delta = max - min;
let hue = 0;
if (delta !== 0) {
if (max === red) {
hue = ((green - blue) / delta) % 6;
}
else if (max === green) {
hue = (blue - red) / delta + 2;
}
else {
hue = (red - green) / delta + 4;
}
hue *= 60;
if (hue < 0) {
hue += 360;
}
}
const saturation = max === 0 ? 0 : (delta / max) * 100;
const value = max * 100;
return {
hue: Math.round(hue),
saturation: Math.round(saturation),
value: Math.round(value),
};
}
async refreshDimmerDetails() {
if (!this.isDimmerDevice) {
return;
}
const dimmer = this.device;
try {
const now = Date.now();
if (now - this.lastDimmerRefresh >= LightAccessory.DIMMER_REFRESH_DEBOUNCE_MS &&
typeof dimmer.getDetails === 'function') {
await dimmer.getDetails();
this.lastDimmerRefresh = now;
}
}
catch (error) {
this.platform.log.debug(`${this.device.deviceName}: failed to refresh details after state change`, error instanceof Error ? error : new Error(String(error)));
}
}
resolveDimmerOnBrightness() {
var _a, _b, _c;
const dimmer = this.device;
const current = Number(dimmer.brightness);
if (!Number.isNaN(current) && current > 0) {
this.lastKnownDimmerBrightness = Math.min(100, Math.max(1, current));
return this.lastKnownDimmerBrightness;
}
const persisted = Number((_c = (_b = (_a = this.accessory.context) === null || _a === void 0 ? void 0 : _a.device) === null || _b === void 0 ? void 0 : _b.details) === null || _c === void 0 ? void 0 : _c.brightness);
if (!Number.isNaN(persisted) && persisted > 0) {
this.lastKnownDimmerBrightness = Math.min(100, Math.max(1, persisted));
return this.lastKnownDimmerBrightness;
}
return Math.min(100, Math.max(1, this.lastKnownDimmerBrightness));
}
}
exports.LightAccessory = LightAccessory;
LightAccessory.DIMMER_REFRESH_DEBOUNCE_MS = 2000;
//# sourceMappingURL=light.accessory.js.map