From 8c6658f3ba992012b89ff4687d6b59065389dd8e Mon Sep 17 00:00:00 2001 From: Ali Hosseini Khayat Date: Sat, 21 Oct 2023 11:37:31 -0700 Subject: [PATCH] feat: add support for rgbcw bulbs bulbs with hardware version 0x0e are RGBCW bulbs which are currently unsupported. add support for these types of bulbs and enable setting the color temperature directly, transforming between mired (micro-reciprocal degrees) as used by HomeKit and white values, rather than transforming HSL values. --- src/accessories/RGBCWBulb.ts | 102 ++++++++++++++++++++++++++++ src/magichome-interface/LightMap.ts | 11 +++ src/magichome-interface/types.ts | 1 + src/magichome-interface/utils.ts | 57 ++++++++++++++++ src/platform.ts | 2 + src/platformAccessory.ts | 36 +++++++++- 6 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 src/accessories/RGBCWBulb.ts diff --git a/src/accessories/RGBCWBulb.ts b/src/accessories/RGBCWBulb.ts new file mode 100644 index 0000000..4d54100 --- /dev/null +++ b/src/accessories/RGBCWBulb.ts @@ -0,0 +1,102 @@ +import { + clamp, + convertHSLtoRGB, + convertMiredToTempInKelvin, + convertRGBtoHSL, + convertTempInKelvinToWhiteValues, +} from '../magichome-interface/utils'; +import { HomebridgeMagichomeDynamicPlatformAccessory } from '../platformAccessory'; + +export class RGBCWBulb extends HomebridgeMagichomeDynamicPlatformAccessory { + async updateDeviceState(_timeout = 200) { + //**** local variables ****\\ + const hsl = this.lightState.HSL; + const isColorTempChange = this.setColortemp; + let [red, green, blue] = [0, 0, 0]; + if (!isColorTempChange) { + [red, green, blue] = convertHSLtoRGB(hsl); //convert HSL to RGB + } + const brightness = this.lightState.brightness; + const mask = isColorTempChange ? 0x0f : 0xf0; // the 'mask' byte tells the controller which LEDs to turn on color(0xF0), white (0x0F), or both (0xFF) + //we default the mask to turn on color. Other values can still be set, they just wont turn on + + //sanitize our color/white values with Math.round and clamp between 0 and 255, not sure if either is needed + //next determine brightness by dividing by 100 and multiplying it back in as brightness (0-100) + const r = Math.round((clamp(red, 0, 255) / 100) * brightness); + const g = Math.round((clamp(green, 0, 255) / 100) * brightness); + const b = Math.round((clamp(blue, 0, 255) / 100) * brightness); + + const temperatureInKelvin = convertMiredToTempInKelvin(this.lightState.CCT); + const brightnessPercentage = brightness / 100; + + const whiteValues = convertTempInKelvinToWhiteValues( + temperatureInKelvin, + brightnessPercentage, + ); + + await this.send( + [0x31, r, g, b, whiteValues.warmWhite, whiteValues.coldWhite, mask, mask], + true, + _timeout, + ); //9th byte checksum calculated later in send() + } + + async updateHomekitState() { + this.service.updateCharacteristic( + this.platform.Characteristic.On, + this.lightState.isOn, + ); + this.service.updateCharacteristic( + this.platform.Characteristic.Hue, + this.lightState.HSL.hue, + ); + this.service.updateCharacteristic( + this.platform.Characteristic.Saturation, + this.lightState.HSL.saturation, + ); + if (this.lightState.HSL.luminance > 0 && this.lightState.isOn) { + this.service.updateCharacteristic( + this.platform.Characteristic.Brightness, + this.lightState.HSL.luminance * 2, + ); + } else if (this.lightState.isOn) { + this.service.updateCharacteristic( + this.platform.Characteristic.Brightness, + clamp( + this.lightState.whiteValues.coldWhite / 2.55 + + this.lightState.whiteValues.warmWhite / 2.55, + 0, + 100, + ), + ); + if ( + this.lightState.whiteValues.warmWhite > + this.lightState.whiteValues.coldWhite + ) { + this.service.updateCharacteristic( + this.platform.Characteristic.Saturation, + this.colorWhiteThreshold - + this.colorWhiteThreshold * + (this.lightState.whiteValues.coldWhite / 255), + ); + this.service.updateCharacteristic(this.platform.Characteristic.Hue, 0); + } else { + this.service.updateCharacteristic( + this.platform.Characteristic.Saturation, + this.colorWhiteThreshold - + this.colorWhiteThreshold * + (this.lightState.whiteValues.warmWhite / 255), + ); + this.service.updateCharacteristic( + this.platform.Characteristic.Hue, + 180, + ); + } + } + this.service.updateCharacteristic( + this.platform.Characteristic.ColorTemperature, + this.lightState.CCT, + ); + this.cacheCurrentLightState(); + } +} diff --git a/src/magichome-interface/LightMap.ts b/src/magichome-interface/LightMap.ts index dd31bbf..6fc8687 100644 --- a/src/magichome-interface/LightMap.ts +++ b/src/magichome-interface/LightMap.ts @@ -45,6 +45,17 @@ const lightTypesMap: Map = new Map([ hasBrightness: true, }, ], + [ + 0x0e, + { + controllerLogicType: ControllerTypes.RGBCWBulb, + convenientName: 'RGBCW Bulb', + simultaneousCCT: false, + hasColor: true, + hasCCT: true, + hasBrightness: true, + }, + ], [ 0x21, { diff --git a/src/magichome-interface/types.ts b/src/magichome-interface/types.ts index 6e5a018..61e640a 100644 --- a/src/magichome-interface/types.ts +++ b/src/magichome-interface/types.ts @@ -28,6 +28,7 @@ export enum ControllerTypes { DimmerStrip = 'DimmerStrip', GRBStrip = 'GRBStrip', RGBWWBulb = 'RGBWWBulb', + RGBCWBulb = 'RGBCWBulb', RGBWBulb = 'RGBWBulb', Switch = 'Switch', RGBStrip = 'RGBStrip' diff --git a/src/magichome-interface/utils.ts b/src/magichome-interface/utils.ts index 2471453..fc3053c 100644 --- a/src/magichome-interface/utils.ts +++ b/src/magichome-interface/utils.ts @@ -137,6 +137,63 @@ export function convertHSLtoRGB ({hue, saturation, luminance}) { //================================================= // End Convert HSLtoRGB // +const MIN_TEMPERATURE_IN_KELVIN = 2000; +const MAX_TEMPERATURE_IN_KELVIN = 7200; + +/** + * Converts white values to temperature in degrees Kelvin and associated brightness percentage. + * @param whiteValues byte values for warm white and cold white + * @returns temperature in degrees Kelvin and brightness percentage + */ +export function convertWhiteValuesToTempInKelvinAndBrightness( + whiteValues: { warmWhite: number; coldWhite: number }, + minTemp = MIN_TEMPERATURE_IN_KELVIN, + maxTemp = MAX_TEMPERATURE_IN_KELVIN, +): { temperature: number; brightnessPercentage: number } { + let temperature = minTemp; + const warm = whiteValues.warmWhite / 255; + const cold = whiteValues.coldWhite / 255; + const brightness = cold + warm; + if (brightness !== 0) { + temperature = (cold / brightness) * (maxTemp - minTemp) + minTemp; + } + return { temperature: temperature, brightnessPercentage: brightness * 100 }; +} + +/** + * Converts temperature in degrees Kelvin and associated brightness percentage to byte values for warm white and cold white + * @param kelvin temperature in degrees Kelvin + * @param brightnessPercentage brightness as a percentage value + * @returns byte values for warm white and cold white + */ +export function convertTempInKelvinToWhiteValues( + kelvin: number, + brightnessPercentage: number, + minTemp = MIN_TEMPERATURE_IN_KELVIN, + maxTemp = MAX_TEMPERATURE_IN_KELVIN, +): { warmWhite: number; coldWhite: number } { + const warm = + ((maxTemp - kelvin) / (maxTemp - minTemp)) * brightnessPercentage; + const cold = brightnessPercentage - warm; + const ww = Math.round(255 * warm); + const cw = Math.round(255 * cold); + return { warmWhite: ww, coldWhite: cw }; +} + +/** + * Converts from temperature in Kelvin (ex. 6500) to mired (micro-reciprocal degrees), as used by HomeKit + */ +export function convertTempInKelvinToMired(kelvin: number): number { + return 1000000 / kelvin; +} + +/** + * Converts from mired (micro-reciprocal degrees), as used by HomeKit, to temperature in Kelvin (ex. 6500) + */ +export function convertMiredToTempInKelvin(mired: number) { + return 1000000 / mired; +} + export function parseJson(value: string, replacement: T): T { try { return JSON.parse(value); diff --git a/src/platform.ts b/src/platform.ts index 932b975..c322a38 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -9,6 +9,7 @@ import { RGBStrip } from './accessories/RGBStrip'; import { GRBStrip } from './accessories/GRBStrip'; import { RGBWBulb } from './accessories/RGBWBulb'; import { RGBWWBulb } from './accessories/RGBWWBulb'; +import { RGBCWBulb } from './accessories/RGBCWBulb'; import { RGBWStrip } from './accessories/RGBWStrip'; import { RGBWWStrip } from './accessories/RGBWWStrip'; import { CCTStrip } from './accessories/CCTStrip'; @@ -30,6 +31,7 @@ const accessoryType = { RGBStrip, RGBWBulb, RGBWWBulb, + RGBCWBulb, RGBWStrip, RGBWWStrip, CCTStrip, diff --git a/src/platformAccessory.ts b/src/platformAccessory.ts index 0a55753..1cd9dcd 100644 --- a/src/platformAccessory.ts +++ b/src/platformAccessory.ts @@ -3,7 +3,13 @@ import type { Service, PlatformConfig, PlatformAccessory, CharacteristicValue, CharacteristicSetCallback, CharacteristicGetCallback, } from 'homebridge'; -import { clamp, convertHSLtoRGB, convertRGBtoHSL } from './magichome-interface/utils'; +import { + clamp, + convertHSLtoRGB, + convertRGBtoHSL, + convertTempInKelvinToMired, + convertWhiteValuesToTempInKelvinAndBrightness, +} from './magichome-interface/utils'; import { HomebridgeMagichomeDynamicPlatform } from './platform'; import { Transport } from './magichome-interface/Transport'; import { getLogs } from './logs'; @@ -249,7 +255,7 @@ export class HomebridgeMagichomeDynamicPlatformAccessory { const CCT = this.lightState.CCT; //update state with actual values asynchronously - this.logs.debug('Get Characteristic Hue -> %o for device: %o ', CCT, this.myDevice.displayName); + this.logs.debug('Get Characteristic Color Temperature -> %o for device: %o ', CCT, this.myDevice.displayName); if(this.setColortemp){ this.updateLocalState(); } @@ -323,6 +329,7 @@ export class HomebridgeMagichomeDynamicPlatformAccessory { this.updateLocalHSL(convertRGBtoHSL(this.lightState.RGB)); this.updateLocalWhiteValues(state.whiteValues); this.updateLocalIsOn(state.isOn); + this.updateLocalColorTemperature(state.whiteValues); this.updateHomekitState(); } catch (error) { @@ -340,10 +347,20 @@ export class HomebridgeMagichomeDynamicPlatformAccessory { this.service.updateCharacteristic(this.platform.Characteristic.On, this.lightState.isOn); this.service.updateCharacteristic(this.platform.Characteristic.Hue, this.lightState.HSL.hue); this.service.updateCharacteristic(this.platform.Characteristic.Saturation, this.lightState.HSL.saturation); - if(this.lightState.HSL.luminance > 0 && this.lightState.isOn){ + if ( + !this.myDevice.lightParameters.hasCCT && + this.lightState.HSL.luminance > 0 && + this.lightState.isOn + ) { this.updateLocalBrightness(this.lightState.HSL.luminance * 2); } this.service.updateCharacteristic(this.platform.Characteristic.Brightness, this.lightState.brightness); + if (this.myDevice.lightParameters.hasCCT) { + this.service.updateCharacteristic( + this.platform.Characteristic.ColorTemperature, + this.lightState.CCT, + ); + } } updateLocalHSL(_hsl){ @@ -366,6 +383,19 @@ export class HomebridgeMagichomeDynamicPlatformAccessory { this.lightState.brightness = _brightness; } + updateLocalColorTemperature(_whiteValues: { + warmWhite: number; + coldWhite: number; + }) { + const tempKelvinAndBrightness = + convertWhiteValuesToTempInKelvinAndBrightness(_whiteValues); + + // convert to mired (micro-reciprocal degrees) + this.lightState.CCT = convertTempInKelvinToMired( + tempKelvinAndBrightness.temperature, + ); + this.lightState.brightness = tempKelvinAndBrightness.brightnessPercentage; + } /** ** @updateDeviceState