openhab / openhab-core

Core framework of openHAB
https://www.openhab.org/
Eclipse Public License 2.0
932 stars 429 forks source link

[Sitemap] Widget for color temperature #3891

Open mueller-ma opened 1 year ago

mueller-ma commented 1 year ago

Is your feature request related to a problem? Please describe.

For some time I created two sliders for lightbulbs that can change their color temperature. One for brightness, one for color temperature. Recently I acquired a Nanoleaf Elements light (LEDs with brightness and color temperature settings). The binding allows to use a color picker to control both brightness and color temperature settings. All colors expect in the range cold white to warm white are ignored by the binding, but I can control both with just one widget.

Describe the solution you'd like

Add a new option to the Color widget (e.g. temperatureOnly) that limits the color selection from cold white to warm white.

Example from the Ikea Tradfri app: tradfri

Describe alternatives you've considered

Additional context

Coordination between maintainers

Notify maintainers of other UIs: @openhab/webui-maintainers @openhab/android-maintainers @openhab/ios-maintainers

Checklist for implementation:

lolodomo commented 1 year ago

For a color item, we have already the colorpicker widget to select the color, the slider widget to select its brightness and the switch widget to turn it on or off.

For color temperature, we could imagine either a new widget or an option to the existing colorpicker widget. In both cases, we need to implement a new UI in all apps. It could be a simple slider ? As a min and max values are probably required, a new widget similar to slider widget would be better I believe. We could name it colortemperature.

lolodomo commented 1 year ago

To be thought how an UI can then build the command to be sent to the server.

mueller-ma commented 1 year ago

For a color item, we have already the colorpicker widget to select the color

The colorpicker can also control the brightness, which is very nice.

When making the color temperature a new option for the colorpicker, we solve two issues:

  1. The state is transmitted as HSB and the binding then has to parse brightness and color temperature from that single state.
  2. Clients without updates show the current color picker which is still useful. It also shows unsupported colors, but better than nothing.
maniac103 commented 1 year ago

The state is transmitted as HSB and the binding then has to parse brightness and color temperature from that single state.

Color temperature channels are either Dimmers or Numbers though, not Colors, so I don't think that idea will fly.

For controlling color temperature channels, the client would need to differentiate between dimmer (relative) or absolute channels (by item type):

mueller-ma commented 12 months ago

Ok, so then it's required to add two items to one widget, like ColorTemperature brightnessItem=foo temperatureItem=bar. This is probably the first widget with more than one item. Is this an argument against it?

andrewfg commented 12 months ago

You could consider to do it like the Philips Hue App -- see screenshots below..

image

image

lolodomo commented 1 month ago

We have two system channel types for color temperature. The first expects a Dimmer item: https://github.com/openhab/openhab-core/blob/6d0a3b330c48cdb1d6112bcf001bed3f11ed4094/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/DefaultSystemChannelTypeProvider.java#L233 The second expects a Number:Temperature item: https://github.com/openhab/openhab-core/blob/6d0a3b330c48cdb1d6112bcf001bed3f11ed4094/bundles/org.openhab.core.thing/src/main/java/org/openhab/core/thing/DefaultSystemChannelTypeProvider.java#L244 For both of them, it looks natural to use a Slider widget.

But I agree that for the second we could imagine a new dedicated widget showing the rendering of white, I imagine it as a slider + an image showing a gradient of temperatures but each app could implement it as it prefers. The idea would be to handle a kelvin (or mireds) value. Are you ok with the idea of a new sitemap element for that ? For Baisc UI, I need to find an image showing a color gradient between 1000 and 10000 K. The widget will look similar to the color widget but with a rectangle instead of a circle. Something like: https://upload.wikimedia.org/wikipedia/commons/e/e9/Color_temperature_black_body_800-12200K.svg

lolodomo commented 1 month ago

For this new sitemap element, I imagine in Basic UI the same rendering as for a Slider element but with an additional button allowing to open a popup to pick a color temperature.

andrewfg commented 1 month ago

@andrewfg : do you know mathematics to convert a kelvin temperature into a RGB value ?

In OH core ColorUtil we have public methods for converting everything to everything, and back. For your specific case of Kelvin to RGB one would use kelvinToXY() => xyToHSB() => hsbToRGB() ..

andrewfg commented 1 month ago

PS apropos this issue in general: As the OP has remarked, most light bindings have two ways of setting the Color Temperature -- namely

It is important to note that in the latter case the respective 0% and 100% points relate to the minimum and maximum Mirek capabilities of the actual lamp being controlled. So, in other words, if you have two lamps from different manufacturers with different colour temperature capabilities, then any particular slider position (say 42%) could relate to a totally different colour temperature.

Therefore for the purposes of this PR I would strongly suggest to focus only on a UI element that produces an absolute QuantityType:Temperature value. i.e. forget about the 0..100% slider stuff..

lolodomo commented 1 month ago

Yes, that is also my idea.

I prefer a separate widget that could also be considered when Default widget is set and the item is of type Number with temperature dimension and has colorTemperature tag.

I would name it Colortemperaturepicker.

@mueller-ma : is it also ok for you?

I can implement the core part quickly, it is only few new lines of code.

lolodomo commented 1 month ago

Reading the first messages, I realize that this will provide only color temperature control, not brightness control in the same widget. As brightness and color temperature are not part of the same item, we have no choice. Having a widget linked to 2 different items would be a massive change. Or we should rethink the Color item to embed optionally color temperature but that would be another big change.

lolodomo commented 1 month ago

But having two widgets, one for brightness and one for colour temperature is acceptable I believe.

andrewfg commented 1 month ago

^ Think of the example of the Hue App (see below) it supports 4 widgets -- namely On/Off, Brightness / Color Picker / Colour Temperature Picker. (Not to mention the other Hue specific widgets for Effects etc.)

Screenshot_20241020_173206_Hue

mueller-ma commented 1 month ago

The issue is mostly above having one widget for brightness and temperature. As I suggested in my first post, the color picker could be limited to temperatures of white.

andrewfg commented 1 month ago

@lolodomo also when you implement something, please remember that the widget output QuantityType:Temperature should NOT produce a linear output in Kelvin. This is because the human eye does not perceive a linear Kelvin transition as a linear visual transition. That is the reason why the inverse Kelvin Mirek unit was invented. The human eye DOES perceive a linear Mirek transition as a linear visual effect.

lolodomo commented 1 month ago

I understand that my proposal does not respond to your need! What would be your concrete proposal that we can implement in OH considering the existing constraints?

lolodomo commented 1 month ago

That is the reason why the inverse Kelvin Mirek unit was invented. The human eye DOES perceive a linear Mirek transition as a linear visual effect.

If this is better, we can consider mired instead of Kelvin.

lolodomo commented 1 month ago

Think of the example of the Hue App (see below) it supports 4 widgets -- namely On/Off, Brightness / Color Picker / Colour Temperature Picker.

So that's fine to have different widgets?

maniac103 commented 1 month ago

As I suggested in my first post, the color picker could be limited to temperatures of white.

I don't see how that would work (in terms of syntax) though. A color picker for color and brightness needs only one item (as brightness is part of color), a color picker for color temperature would need two items (color temperature and brightness). How should core and/or main UI validate that properly?

lolodomo commented 1 month ago

The hue example shows clearly different "widgets", we could have the same approach in openHAB. We already have these existing widgets:

  1. Switch
  2. Slider for brightness
  3. Colour picker + Slider for Brightness => this was possible because color and brightness are combined in the same HSB state.

My proposal is to add a fourth widget for colour temperature (but not combined with brightness)..

andrewfg commented 1 month ago

if this is better, we can consider mired instead of Kelvin.

You must definitely do this. It must produce QuantityType:Temperature where the linear transition from one end of the widget control to the other produces a linear output in Mirek. Typically the lowest Mirek is 153 (aka 6500 Kelvin) and the highest Mirek is 500 (aka 2000 Kelvin)

add a fourth widget for colour temperature (but not combined with brightness)..

Makes sense.

lolodomo commented 1 month ago

@andrewfg : I will have more difficulty to find an image with a gradient in mired instead of Kelvin.

andrewfg commented 1 month ago

more difficulty to find an image with a gradient in mired instead of Kelvin.

Umm. That is not really a reason to do it wrong. And you could use the gradient that the OP copied from Hue above. IMHO the Hue engineers do really have a solid understanding.

lolodomo commented 1 month ago

I found this one but the values looks wrong, like negative. https://commons.wikimedia.org/wiki/File:Color_temperature_black_body_mired.svg

andrewfg commented 1 month ago

^ @lolodomo if you want, I can try to (mis) use the ColorUtils methods to programmatically produce a table of RGB values that match the proper gradient. Advantage of doing it that way would be getting a perfect match between UI and OH Java code. (Always assuming your display has proper color calibration). I would give you a table with 101 RGB values matching the Mirek scale.

PS you also need to decide the min/max range of the scale. It seems that max Mirek 500 (2000 K) is fairly non controversial. And in OH bindings (resp. actual lamps) min Mirek 153 (6500 K) is common. But others seem to prefer min Mirek 100 (10000 K). The latter would perhaps be more future proof for future much bluer lamps, but would overflow for lamps like Hue or Zigbee ones.

andrewfg commented 1 month ago

the values looks wrong

Agreed. ):

andrewfg commented 1 month ago

I will create some gradients tomorrow. What image format do you prefer (e.g. png etc.) and what width (in pixels)? Note pixel height is trivial since each row is the same..

lolodomo commented 1 month ago

Looking at the current color picker in Basic UI, I see the gradient for brightness dynamically adjusted with the selected colour. I assume it is computed, I have to check how. Maybe I could replace it with a computed gradient of mired values.

If not possible, we could have an image with a linear gradient between 100 and 500 mired. Same width as the colorwheel in the colour picker (to be checked). Colorwheel image is in PNG format so let's go with same format. I will reject selection of temperature outside the min/max of the current light. If the user clicks < min in the image, I will adjust to min. Similar for max.

But if I could build dynamically the gradient, that would be even better.

lolodomo commented 1 month ago

@andrewfg : A PNG file 300x300 pixels would be fine.

Regarding computed gradient for (background) brightness slider, the CSS linear-gradient function is used combined with RGBA values allowing a gradient (based on transparency and a background color). We can't use that for color temperature, at least not the same way but one idea would to use this linear-gradient function and provide to it a good amount of RGB values (10 or 20) to finally get something that will look like the gradient we are looking for. For that, I will need conversion from mired to RGB and I guess I should be able to compute the different values between min and max mired in Java and then provide them to the JavaScript to build the gradient. To be studied. In case it works, no need for an image containing the hardcoded gradient.

maniac103 commented 1 month ago

An Kelvin-to-RGB algorithm can be found here. Kelvin-to-Mired is easy: Mired * Kelvin = 1000000.

andrewfg commented 1 month ago

Kelvin-to-RGB algorithm can be found

We already have enough conversions in OH Core ColorUtils. There is no need to reinvent the wheel..

andrewfg commented 1 month ago

need conversion from mired to RGB

Try this..

rgb = ColorUtil.hsbToRGB(ColorUtil.xyToHSB(ColorUtil.kelvinToXY(1000000 / mirek)))
lolodomo commented 1 month ago

I will first define the new sitemap element in core framework if that's fine for everybody.

lolodomo commented 1 month ago

@andrewfg : don't loose your time for me, I believe I should be able to create a good linear gradient using the CSS function linear-gradient. So I don't need any PNG image.

andrewfg commented 1 month ago

create a good linear gradient using the CSS function linear-gradient.

I agree. If I would have done it, I would have created an SVG image that uses the same linear gradient function. Probably the only thing you need to tweak is the step size for the sub gradient parts. I would perhaps start with 20 sub gradients at step size of 5% .. but eventually it could go smaller (or larger)..

andrewfg commented 1 month ago

Just to mention that the ColorUtil.kelvinToXy() method currently only handles 2000 K to 6500 K (500 Mk to 153 Mk) so it won’t return proper RGB values for higher colour temperatures. And if you eventually want the control to go to higher colour temperatures (e.g. 10000 K / 100 Mk) then I shall need to extend the internal lookup table. It is easily done, so let me know.

https://github.com/openhab/openhab-core/blob/a22349abf4b3106ec8a3eb9d36799e334cdfbf25/bundles/org.openhab.core/src/main/java/org/openhab/core/util/ColorUtil.java#L1187

lolodomo commented 1 month ago

In practice, range 2000-6500 K should probably be fine. But the default range of the color temperature system channel is 1000-10000 K.

andrewfg commented 1 month ago

^ @lolodomo OK. Let me know if you need any changes from my side.

lolodomo commented 1 month ago

What I could do is a failback to a standard slider in case the range is larger than 2000-6500 K.

lolodomo commented 1 month ago

What I could do is a failback to a standard slider in case the range is larger than 2000-6500 K.

No, we can't do that as this will trigger the failback for any binding using the system channel type.

@andrewfg : I believe the best option is that you extend the range please.

andrewfg commented 1 month ago

the best option is that you extend the range please.

Ok. I will do it.

andrewfg commented 1 month ago

@lolodomo following is a link to my PR to extend the colour temperature range of the kelvinToXy() method from 2000 .. 6500 K to 1000 .. 10000 K

https://github.com/openhab/openhab-core/pull/4429


Please note that the inverse xyToKelvin() method (which is based on McCamy's approximation) is only accurate above 2000 K. This means that round trip conversions xyToKelvin(kelvinToXy()) are very inaccurate for colour temperatures below 2000 K. This is not perfect but, as we have observed above, it seems that most real bindings in OH are using a range of 2000 K to 6500 K .. where the accuracy is better than 1%..

lolodomo commented 1 month ago

@andrewfg : ok, no problem.

Here is the current result with a step of 5% between 154 and 400 mired (2500 K to 6500 K):

image

Looks not so bad,no ? Do you think I should provide more values ?

andrewfg commented 1 month ago

It would be interesting to see if 100 Mk results in a blueish colour, as I would expect..

lolodomo commented 1 month ago

With the current code (I mean without your extended values), it leads to that for a gradient between 100 mired and 1000 mired (1000 K to 10000 K).

image

All values outside the supported range are leading to an exception. It leads to that:

rgba(255, 251, 234, 1) 10%, rgba(255, 239, 199, 1) 15%, rgba(255, 228, 169, 1) 20%, rgba(255, 217, 141, 1) 25%, rgba(255, 206, 118, 1) 30%, rgba(255, 196, 97, 1) 35%, rgba(255, 186, 80, 1) 40%
lolodomo commented 1 month ago

I am asking myself if I have the expected values. As you say, we should have more blue white ?

For the "standard" range 154-400 mired, the computed values are:

rgba(244, 250, 255, 1) 0%, rgba(253, 255, 253, 1) 5%, rgba(255, 254, 244, 1) 10%, rgba(255, 251, 234, 1) 15%, rgba(255, 248, 224, 1) 20%, rgba(255, 245, 214, 1) 25%, rgba(255, 241, 205, 1) 30%, rgba(255, 238, 196, 1) 35%, rgba(255, 235, 187, 1) 40%, rgba(255, 232, 178, 1) 45%, rgba(255, 229, 171, 1) 50%, rgba(255, 226, 163, 1) 55%, rgba(255, 223, 155, 1) 60%, rgba(255, 219, 147, 1) 65%, rgba(255, 217, 141, 1) 70%, rgba(255, 214, 134, 1) 75%, rgba(255, 210, 127, 1) 80%, rgba(255, 207, 121, 1) 85%, rgba(255, 205, 115, 1) 90%, rgba(255, 201, 109, 1) 95%, rgba(255, 199, 104, 1) 100%
lolodomo commented 1 month ago

My current code:

                        long minMired = Units.KELVIN.toString().equals(unit) ? Math.round(1000000.0 / max.longValue())
                                : min.longValue();
                        long maxMired = Units.KELVIN.toString().equals(unit) ? Math.round(1000000.0 / min.longValue())
                                : max.longValue();
                        StringBuilder gradientBuilder = new StringBuilder();
                        for (int percent = 0; percent <= 100; percent += 5) {
                            double valueMired = (maxMired - minMired) * percent / 100.0 + minMired;
                            double valueKelvin = 1000000.0 / valueMired;
                            try {
                                int[] rgb = ColorUtil.hsbToRgb(ColorUtil.xyToHsb(ColorUtil.kelvinToXY(valueKelvin)));
                                logger.debug("Gradient {}%: {} mired => {} K => RGB {} {} {}", percent, valueMired,
                                        valueKelvin, rgb[0], rgb[1], rgb[2]);
                                gradientBuilder.append(
                                        "rgba(%d, %d, %d, 1) %d%%, ".formatted(rgb[0], rgb[1], rgb[2], percent));
                            } catch (IllegalArgumentException | IndexOutOfBoundsException e) {
                                logger.debug("Can't get RGB for {} Kelvin, ignoring it", valueKelvin);
                            }
                        }
                        if (gradientBuilder.length() > 2) {
                            gradientColors = gradientBuilder.substring(0, gradientBuilder.length() - 2);
                        }
andrewfg commented 1 month ago

@lolodomo can you please advise how you would plan to set the min/max Mirek range of the CT UI slider? Do you plan to have a fixed range ( 100 .. 1000 )? And/or will the UI control have a configuration screen to override the min/max?

Reason why I ask is that some bindings can provide the range from within code. For example the Hue binding API V2 provides min/max Mirek on a lamp by lamp basis. So more performant lamps could show a wider UI gradient than less performant ones. We would need, however, to create a mechanism whereby the UI can query the Item and in turn query the Channel to get meta data about the respective actual min/max range..

lolodomo commented 1 month ago

Min and max are two parameters of the new sitemap element. So you can choose the range you like in the sitemap. If not set, I try to get the range from the item state description. This will be the standard use case I believe. I finally consider the range 100-1000 mired as default if nothing is defined in sitemap/item.