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