Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Color Temperature #26

Closed
digitaldan opened this issue Apr 27, 2017 · 13 comments
Closed

Support Color Temperature #26

digitaldan opened this issue Apr 27, 2017 · 13 comments

Comments

@digitaldan
Copy link
Collaborator

Changes to Smart Home Skill API Reference - April 7, 2017

New directives added to control the color and tunable white light settings of devices
SetColorTemperatureRequest
SetColorTemperatureConfirmation
IncrementColorTemperatureRequest
IncrementColorTemperatureConfirmation
DecrementColorTemperatureRequest
DecrementColorTemperatureConfirmation

@fourthwall
Copy link
Contributor

Hi Dan - I had a think about this one...

It seems that for the lights that support colour temperature in OpenHAB, the binding exposes them as a dimmer - but I would imagine the colour temperature is in discreet steps - my milight bulbs are - but then again, they are cheap :) perhaps the tradfri bulbs are a smooth slider of colour temperature.

Anyway, we'd need some way of getting the low and high points of white balance to be able to translate a specific colour temperature to a percentage value for OpenHAB. Are the ways to do this in OpenHAB?

@jsetton
Copy link
Collaborator

jsetton commented Feb 4, 2018

In case, this support will not be part of the initial release of the v3 ported skill [#54], I have implemented the color temperature support with the current version.

jsetton/openhab-alexa@8de8eca

In my implementation, the color temperature is defined as a dimmer item which is set to the value given by the API divided by 100. The increment/decrement functionalities send INCREASE/DECREASE command to that dimmer item so that the increment logic can be handled in OH since the api request is not providing an increment value.

The associated item can be Color, Dimmer, or even Switch, and has to be tagged with "Lighting". Both items need to be in the same group. Here is an item definition example:

Group gHueLight
Color hueLight "Hue Light" (gHueLight) ['Lighting']
Dimmer hueLightColorTemperature "Hue Light Color Temperature" (gHueLight) ['ColorTemperature']

Group gDimmerLight
Dimmer dimmerLight "Dimmer Light" (gDimmerLight) ['Lighting']
Dimmer dimmerLightColorTemperature "Dimmer Light Color Temperature" (gDimmerLight) ['ColorTemperature']

Group gSwitchLight
Switch switchLight "Switch Light" (gSwitchLight) ['Lighting']
Dimmer switchLightColorTemperature "Switch Light Color Temperature" (gSwitchLight) ['ColorTemperature']

Additionally, it handles the not supported in color mode case, when a color temperature item receives an increment request while the associated color item is that mode, as defined in the API reference. This assumes that on the OH side, the color temperature item state would be updated to 0 or NULL, when a non-white colored command is received by the associated color item.

@AfromD
Copy link

AfromD commented Feb 10, 2018

@jsetton : Can I somehow help in testing this? I am using the zigbee binding to control some lights, and have been waiting to be able to control color temperature via alexa.

@jsetton
Copy link
Collaborator

jsetton commented Feb 10, 2018

@AfromD You can run your own version of the skill and back-end function by cloning my repository and using the installation steps listed in the README.

Keep in mind that if going that route, you would need to use this workaround to create a new v2 skill in your Amazon developer console.

I have tested successfully this implementation with my cabinet RGBW LED strips using the WiFi LED binding and a color temperature to RGBW conversion algorithm since the color and white controls are separated with that binding. I can share my setup if interested.

@AfromD
Copy link

AfromD commented Feb 12, 2018

@jsetton Thanks for your work. Right now it seems like a steep learning curve to get it going, probably won't find time for that soon. Was hoping there was some way I could have used an already set up (unpublished) instance.

@djdubois
Copy link

@jsetton Thanks for sharing your code. I set your skill up. Not super easy, but eventually I had it run. Unfortunately the device I need to control (a WiFi LED strip compatible with the WiFi LED binding, but only RGB - not RGBW) does not have the white channel for the temperature. The native app (MagicHome) is able to emulate all the whites through Alexa by converting the shades of whites to HSL values.

Do you know if there is way to let the Alexa app emulate the white through a simple color adjustment? May it be feasible to adapt the Alexa skill code in such a way that tagging the Color device itself (instead of the dimmer) with 'ColorTemperature' creates some sort of "color temperature emulation"? NB: I am not asking you to do it, but just to let me (and other interested readers know if it is feasible without having to modify OH's bindings).

@jsetton
Copy link
Collaborator

jsetton commented Feb 12, 2018

@djdubois I actually did exactly what you are describing in emulating the Magic Home Alexa skill to a certain extent on the OH side. I don't believe this should be implemented in the skill as the color temperature integration will be different depending on the binding and lights used. To limit the complexity, I could see some portion of the logic that I provided below moved to the WiFi LED binding code but that would require some additional work.

You can use the below color temperature to RGB algorithm to get the emulated white color code. On my side, I just have an additional algorithm which translates RGB to RGBW. I also included the rule functions I am using to control my LED strips. Obviously, the change color logic would be much simpler for your setup but I would still recommend using a virtual color item and a function so the non-white and white color commands can be differentiated. As I mentioned above, the color temperature item should be updated to 0 when setting to a non-white color so that the skill can determine if in color mode.

As you can see, I initially had hardcoded values but wanted a more linear increment functionality to work with the skill opposed to just cycling through the color temperature values used by the Smart Home API. I also have a color temperature offset (colorTemperature + 2500) because my white leds are warm and the color temperature conversion rendering was too warm to my taste.

transform/colorTempToRGB.js

(function(kelvin) {
  // Convert Color Temperature to RGB using algorithm referenced in this post:
  //   http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/

  var temp = kelvin / 100;
  var red, green, blue;

  if (temp == 0) {
    red = green = blue = 0;
  } else if (temp <= 66) {
    red = 255;
    green = Math.round(99.4708025861 * Math.log(temp) - 161.1195681661);
    blue = Math.round(temp <= 19 ? 0 : 138.5177312231 * Math.log(temp - 10) - 305.0447927307);
  } else {
    red = Math.round(329.698727446 * Math.pow(temp - 60, -0.1332047592));
    green = Math.round(288.1221695283 * Math.pow(temp - 60, -0.0755148492));
    blue = 255;
  }

  // Clamp color values to 8-bits
  red = red > 0 ? red < 255 ? red : 255 : 0;
  green = green > 0 ? green < 255 ? green : 255 : 0;
  blue = blue > 0 ? blue < 255 ? blue : 255 : 0;

  return red + "," + green + "," + blue;
})(input)

rules/wifiled.rules

// Change Cabinet Color Function
val Functions$Function2<ColorItem, Object, Void> changeCabinetColor = [
  cabinetItem, receivedCommand |
    val cabinetName = cabinetItem.name
    val cabinetGroupName = cabinetItem.groupNames.findFirst[g | g.contains(cabinetName)]
    val cabinetGroup = gCabinetLights.members.findFirst[g | g.name == cabinetGroupName] as GroupItem
    val cabinetPower = cabinetGroup.members.findFirst[i | i.name == cabinetName + "Power"] as SwitchItem
    val cabinetColor = cabinetGroup.members.findFirst[i | i.name == cabinetName + "Color"] as ColorItem
    val cabinetWhite = cabinetGroup.members.findFirst[i | i.name == cabinetName + "White"] as DimmerItem

    // Turn off power if receive OFF, 0,0,0 (Black color) or 0 (Zero level)
    if (receivedCommand instanceof OnOffType && receivedCommand == OFF ||
          receivedCommand instanceof HSBType && receivedCommand == HSBType::BLACK ||
            receivedCommand instanceof PercentType && receivedCommand == PercentType::ZERO) {
      cabinetPower.sendCommand(OFF)
    } else {
      // Turn on power if not already
      if (cabinetPower.state != ON)
        cabinetPower.sendCommand(ON)

      // Set Cabinet Color/White level depending on receivedCommand
      if (receivedCommand instanceof HSBType) {
        // Convert RGB color to RGBW
        val rgbw = transform("JS", "colorRGBToRGBW.js",
          receivedCommand.red + ',' + receivedCommand.green + ',' + receivedCommand.blue)

        //logInfo(cabinetName + "Color", "RGB: {},{},{} => RGBW: {}",
        //  (receivedCommand.red * 2.55).intValue, (receivedCommand.green * 2.55).intValue,
        //  (receivedCommand.blue * 2.55).intValue, rgbw)

        // Seperate colors
        val red = Integer::parseInt(rgbw.split(',').get(0))
        val green = Integer::parseInt(rgbw.split(',').get(1))
        val blue = Integer::parseInt(rgbw.split(',').get(2))
        val white = Integer::parseInt(rgbw.split(',').get(3))

        // Set Cabinet Color/White
        cabinetColor.sendCommand(HSBType::fromRGB(red, green, blue))
        cabinetWhite.sendCommand(white)

      } else if (receivedCommand instanceof PercentType) {
        val color = cabinetColor.state as HSBType
        val brightness = Math::round((receivedCommand * color.brightness / (color.brightness + cabinetWhite.state)).floatValue)
        val white = Math::round((receivedCommand * cabinetWhite.state / (color.brightness + cabinetWhite.state)).floatValue)

        //logInfo(cabinetName + "Color", "Color Level: {} => {}", color.brightness, brightness)
        //logInfo(cabinetName + "Color", "White Level: {} => {}", cabinetWhite.state, white)

        cabinetColor.sendCommand(new HSBType(color.hue, color.saturation, new PercentType(brightness)))
        cabinetWhite.sendCommand(white)
      }
    }
]

// Change Cabinet Color Temperature Function
val Functions$Function3<ColorItem, Number, Functions$Function2<ColorItem, Object, Void>, Void> changeCabinetColorTemperature = [
  cabinetItem, colorTemperature, changeFunction |
    // Convert color temperature to RGB
    val rgb = transform("JS", "colorTempToRGB.js", colorTemperature.toString)

    //logInfo(cabinetItem.name + "ColorTemperature", "RGB: {}", rgb)

    // Seperate colors
    val red = Integer::parseInt(rgb.split(',').get(0))
    val green = Integer::parseInt(rgb.split(',').get(1))
    val blue = Integer::parseInt(rgb.split(',').get(2))

    // Set Cabinet color
    changeFunction.apply(cabinetItem, HSBType::fromRGB(red, green, blue))

    // Hardcoded value used by Magic Home Alexa skill (RGBW)
    /* switch colorTemperature {
      case 2200: changeFunction.apply(cabinetItem, "0,0,100")    // Warm White
      case 2700: changeFunction.apply(cabinetItem, "28,65,100")  // Soft White
      case 4000: changeFunction.apply(cabinetItem, "26,25,100")  // White
      case 5500: changeFunction.apply(cabinetItem, "260,3,100")  // Daylight White
      case 7000: changeFunction.apply(cabinetItem, "222,18,100") // Cool White
    } */
]

// Get Color Temperature Function
val Functions$Function2<Object, Object, Number> getColorTemperature = [
  colorTemperatureState, receivedCommand |
    var colorTemperature = 0;
    val defaultIncrement = 500;
    val defaultValue = 4000;
    val minValue = 1000;
    val maxValue = 10000;
    val curValue = if (colorTemperatureState instanceof PercentType) (colorTemperatureState as DecimalType).intValue * 100 else -1

    if (receivedCommand instanceof OnOffType) {
      colorTemperature = if (receivedCommand == ON) defaultValue else 0
    } else if (receivedCommand instanceof PercentType) {
      val setValue = (receivedCommand as DecimalType).intValue * 100
      colorTemperature = if (setValue > minValue) setValue else minValue
    } else if (receivedCommand instanceof IncreaseDecreaseType) {
      if (curValue >= 0)
        switch receivedCommand {
          case INCREASE: colorTemperature = if (curValue + defaultIncrement < maxValue) curValue + defaultIncrement else maxValue
          case DECREASE: colorTemperature = if (curValue - defaultIncrement > minValue) curValue - defaultIncrement else minValue
        }
    }

    //logInfo(colorTemperatureItem.name, "Color Temperature: {}K", colorTemperature)
    return colorTemperature;
]

rule "Under Cabinet Color Command"
when
  Item UnderCabinet received command
then
  // Reset Color Temperature
  UnderCabinetColorTemperature.postUpdate(0)
  // Change Color
  changeCabinetColor.apply(UnderCabinet, receivedCommand)
end

rule "Under Cabinet Color Temperature Command"
when
  Item UnderCabinetColorTemperature received command
then
  // Determine color temperature
  val colorTemperature = getColorTemperature.apply(UnderCabinetColorTemperature.state, receivedCommand)
  // Update Color Temperature
  UnderCabinetColorTemperature.postUpdate((colorTemperature / 100).intValue)
  // Change Color Temperature
  changeCabinetColorTemperature.apply(UnderCabinet, colorTemperature + 2500, changeCabinetColor)
end

items/wifiled.items

Group gCabinetLights "Cabinet Lights" <light>
Group gUnderCabinetLight "Under Cabinet Light" <light> (gCabinetLights)

Switch UnderCabinetPower "Under Cabinet Power" (gUnderCabinetLight) {channel="wifiled:wifiled:xxxxxxxxxxxx:power"}
Color UnderCabinetColor "Under Cabinet Color" (gUnderCabinetLight) {channel="wifiled:wifiled:xxxxxxxxxxxx:color"}
Dimmer UnderCabinetWhite "Under Cabinet White" (gUnderCabinetLight) {channel="wifiled:wifiled:xxxxxxxxxxxx:white"}

Color UnderCabinet "Under Cabinet Light" (gUnderCabinetLight) ['Lighting']
Dimmer UnderCabinetColorTemperature "Under Cabinet Color Temperature" (gUnderCabinetLight) ['ColorTemperature']

@AnPutz
Copy link

AnPutz commented May 14, 2018

very good solution.
Could you provide colorRGBToRGBW.js aswell please

thx a lot

@jsetton
Copy link
Collaborator

jsetton commented May 15, 2018

@AnPutz Here you go. I also included the reverse logic as well in case you want to keep your virtual color item in sync when you interact directly with your LED lights.

transform/colorRGBToRGBW.js

(function(rgb) {
  // Convert RGB to RGBW using algorithm referenced in this post:
  //   https://stackoverflow.com/questions/40312216/converting-rgb-to-rgbw

  // Convert and split rgb colors from percemt to 8-bits value
  var redInput = Math.round(rgb.split(",")[0] * 255) / 100;
  var greenInput = Math.round(rgb.split(",")[1] * 255) / 100;
  var blueInput = Math.round(rgb.split(",")[2] * 255) / 100;

  // Get the maximum between R, G, and B
  var maxInput = Math.max(redInput, Math.max(greenInput, blueInput));

  // If the maximum value is 0, immediately return pure black.
  if (maxInput == 0)
    return "0,0,0,0";

  // This section serves to figure out what the color with 100% hue is
  var multiplier = 255.0 / maxInput;
  var redHue = redInput * multiplier;
  var greenHue = greenInput * multiplier;
  var blueHue = blueInput * multiplier;

  // This calculates the Whiteness (not strictly speaking Luminance) of the color
  var max = Math.max(redHue, Math.max(greenHue, blueHue));
  var min = Math.min(redHue, Math.min(greenHue, blueHue));
  var luminance = ((max + min) / 2.0 - 127.5) * (255.0 / 127.5) / multiplier;

  // Calculate the output values
  var redOutput = Math.round(redInput - luminance);
  var greenOutput = Math.round(greenInput - luminance);
  var blueOutput = Math.round(blueInput - luminance);
  var whiteOutput = Math.round(luminance / 255.0 * 100);

  // Clamp output values within 8-bits
  redOutput = redOutput > 0 ? redOutput < 255 ? redOutput : 255 : 0;
  greenOutput = greenOutput > 0 ? greenOutput < 255 ? greenOutput : 255 : 0;
  blueOutput = blueOutput > 0 ? blueOutput < 255 ? blueOutput : 255 : 0;

  return redOutput + "," + greenOutput + "," + blueOutput + "," + whiteOutput;
})(input)

transform/colorRGBWToRGB.js

(function(rgbw) {
  // Convert RGBW to RGB reversing algorithm referenced in this post:
  //   https://stackoverflow.com/questions/40312216/converting-rgb-to-rgbw

  // Convert and split rgbw colors from percent to 8-bits value
  var redInput = Math.round(rgbw.split(",")[0] * 255) / 100;
  var greenInput = Math.round(rgbw.split(",")[1] * 255) / 100;
  var blueInput = Math.round(rgbw.split(",")[2] * 255) / 100;
  var whiteInput = Math.round(rgbw.split(",")[3] * 255) / 100;

  // Calculate the output values
  var redOutput = Math.round(redInput + whiteInput);
  var greenOutput = Math.round(greenInput + whiteInput);
  var blueOutput = Math.round(blueInput + whiteInput);

  // Clamp output values within 8-bits
  redOutput = redOutput > 0 ? redOutput < 255 ? redOutput : 255 : 0;
  greenOutput = greenOutput > 0 ? greenOutput < 255 ? greenOutput : 255 : 0;
  blueOutput = blueOutput > 0 ? blueOutput < 255 ? blueOutput : 255 : 0;

  return redOutput + "," + greenOutput + "," + blueOutput;
})(input)

@jsetton
Copy link
Collaborator

jsetton commented May 15, 2018

One aspect I forgot to add is that I changed the item for this logic to be a number instead of a dimmer. So the get color temperature function is no longer needed and the increment values are now coming from the Alexa skill since number items don't support IncreaseDecreaseType commands.

While doing some research on how other devices with color temperature capability are controlled by openHAB natively, I found that the default way is using a dimmer where 0% is coldest and 100% is warmest. This is how it is working in the Hue and LIFX bindings currently. Therefore, I implemented the logic for the color temperature support in v3 for these two types of items.

In the end, I think this logic should be moved to the WiFi LED binding in a form of a color temperature channel.

@AnPutz
Copy link

AnPutz commented May 16, 2018

Thx a lot for your fast response, I will try it next free time slot.

@thedannymullen
Copy link

When will the v3 be released so you have an idea? I ask because I was looking for th is feature and found this request.

The ks!

@digitaldan
Copy link
Collaborator Author

Implemented in v3 (in beta testing now)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants