Skip to content

Commit 2315cf7

Browse files
authored
[LightModel] Add RGBW and RGBCW "no brightness" modes (#5752)
Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
1 parent 16daabb commit 2315cf7

2 files changed

Lines changed: 323 additions & 45 deletions

File tree

bundles/org.openhab.core/src/main/java/org/openhab/core/util/LightModel.java

Lines changed: 61 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.math.BigDecimal;
1616
import java.util.Arrays;
1717
import java.util.Objects;
18+
import java.util.Set;
1819

1920
import org.eclipse.jdt.annotation.NonNullByDefault;
2021
import org.eclipse.jdt.annotation.Nullable;
@@ -63,7 +64,7 @@
6364
* Also set {@link #rgbDataType} to the chosen RGB data type RGB, RGBW, RGBCW etc.
6465
* And optionally set the following configuration parameters:
6566
* <ul>
66-
* <li>Optionally override {@link #minimumOnBrightness} to a minimum brightness percent in the range [0.1..10.0]
67+
* <li>Optionally override {@link #minimumOnBrightness} to a minimum brightness percent in the range [0.0..10.0]
6768
* percent, to consider as being "ON". The default is 1 percent.</li>
6869
* <li>Optionally override {@link #mirekControlWarmest} to a 'warmest' white color temperature in the range
6970
* [{@link #mirekControlCoolest}..1000.0] Mirek/Mired. The default is 500 Mirek/Mired.</li>
@@ -237,9 +238,30 @@ public static enum RgbDataType {
237238
/** supports 4-element RGB with white channel */
238239
RGB_W,
239240
/** supports 5-element RGB with cold and warm white channels */
240-
RGB_C_W
241+
RGB_C_W,
242+
/** as RGB_W but ignores brightness (i.e. only HS parts of HSBType) */
243+
RGB_W_NO_BRIGHTNESS,
244+
/** as RGB_C_W but ignores brightness (i.e. only HS parts of HSBType) */
245+
RGB_C_W_NO_BRIGHTNESS
241246
}
242247

248+
/**
249+
* Set of RGB data types that do not use the brightness part of the HSBType state.
250+
*/
251+
private static final Set<RgbDataType> NO_BRIGHTNESS_TYPES = Set.of(RgbDataType.RGB_NO_BRIGHTNESS,
252+
RgbDataType.RGB_W_NO_BRIGHTNESS, RgbDataType.RGB_C_W_NO_BRIGHTNESS);
253+
254+
/**
255+
* Set of RGB data types that use a white channel.
256+
*/
257+
private static final Set<RgbDataType> RGB_W_TYPES = Set.of(RgbDataType.RGB_W, RgbDataType.RGB_W_NO_BRIGHTNESS);
258+
259+
/**
260+
* Set of RGB data types that use cold and warm white channels.
261+
*/
262+
private static final Set<RgbDataType> RGB_C_W_TYPES = Set.of(RgbDataType.RGB_C_W,
263+
RgbDataType.RGB_C_W_NO_BRIGHTNESS);
264+
243265
/**
244266
* Enum for the LED operating mode
245267
* <p>
@@ -518,9 +540,9 @@ public synchronized void configSetLightCapabilities(LightCapabilities lightCapab
518540
* @throws IllegalArgumentException if the minimumBrightness parameter is out of range.
519541
*/
520542
public synchronized void configSetMinimumOnBrightness(double minimumOnBrightness) throws IllegalArgumentException {
521-
if (minimumOnBrightness < 0.1 || minimumOnBrightness > 10.0) {
543+
if (minimumOnBrightness < 0.0 || minimumOnBrightness > 10.0) {
522544
throw new IllegalArgumentException(
523-
"Minimum brightness '%.1f' out of range [0.1..10.0]".formatted(minimumOnBrightness));
545+
"Minimum brightness '%.1f' out of range [0.0..10.0]".formatted(minimumOnBrightness));
524546
}
525547
this.minimumOnBrightness = minimumOnBrightness;
526548
}
@@ -723,7 +745,7 @@ public synchronized double getMirek() {
723745
*/
724746
public synchronized @Nullable OnOffType getOnOff(boolean forceChannelVisible) {
725747
return (!lightCapabilities.supportsColor() && !lightCapabilities.supportsBrightness()) || forceChannelVisible
726-
? OnOffType.from(cachedHSB.getBrightness().doubleValue() >= minimumOnBrightness)
748+
? OnOffType.from(cachedHSB.getBrightness().doubleValue() > minimumOnBrightness)
727749
: null;
728750
}
729751

@@ -734,20 +756,20 @@ public synchronized double getMirek() {
734756
* follows:
735757
*
736758
* <ul>
737-
* <li>'RGB_NO_BRIGHTNESS': The return result does not depend on the current brightness. In other words the values
738-
* only relate to the 'HS' part of the {@link HSBType} state. Note: this means that in this case a round trip of
739-
* setRGBx() followed by getRGBx() will NOT necessarily contain identical values, although the RGB ratios will
740-
* certainly be the same.</li>
759+
* <li>'RGB_NO_BRIGHTNESS', 'RGB_W_NO_BRIGHTNESS', 'RGB_C_W_NO_BRIGHTNESS': The return result does not depend on
760+
* the current brightness. In other words the values only relate to the 'HS' part of the {@link HSBType} state.
761+
* Note: this means that in this case a round trip of setRGBx() followed by getRGBx() will NOT necessarily contain
762+
* identical values, although the RGB ratios will certainly be the same.</li>
741763
*
742764
* <li>All other values of {@link #rgbDataType}: The return result depends on the current brightness. In other
743765
* words the values relate to all the 'HSB' parts of the {@link HSBType} state.</li>
744-
* <ul>
766+
* </ul>
745767
*
746768
* @return double[] representing the RGB(C)(W) components in range [0..255.0]
747769
* @throws IllegalStateException if the RGB data type is not compatible with the current LED operating mode.
748770
*/
749771
public synchronized double[] getRGBx() throws IllegalStateException {
750-
HSBType hsb = RgbDataType.RGB_NO_BRIGHTNESS == rgbDataType
772+
HSBType hsb = NO_BRIGHTNESS_TYPES.contains(rgbDataType)
751773
? new HSBType(cachedHSB.getHue(), cachedHSB.getSaturation(), PercentType.HUNDRED)
752774
: cachedHSB;
753775

@@ -758,24 +780,24 @@ public synchronized double[] getRGBx() throws IllegalStateException {
758780
/*
759781
* If the light has a single white led then its value is determined by the brightness only.
760782
*/
761-
if (RgbDataType.RGB_W == rgbDataType) {
762-
double w = cachedHSB.getBrightness().doubleValue() * 255.0 / 100.0;
783+
if (RGB_W_TYPES.contains(rgbDataType)) {
784+
double w = hsb.getBrightness().doubleValue() * 255.0 / 100.0;
763785
return new double[] { 0.0, 0.0, 0.0, w };
764786
}
765787

766788
/*
767789
* If the light has a warm and a cool white led, the mix of white values are determined
768790
* by the brightness and the color temperature.
769791
*/
770-
if (RgbDataType.RGB_C_W == rgbDataType) {
792+
if (RGB_C_W_TYPES.contains(rgbDataType)) {
771793
double denominator = warmWhiteLed.getMirek() - coolWhiteLed.getMirek();
772794
double ratio;
773795
if (denominator > 0 && !Double.isNaN(cachedMirek)) {
774796
ratio = Math.max(0.0, Math.min(1.0, (cachedMirek - coolWhiteLed.getMirek()) / denominator));
775797
} else {
776798
ratio = 0.5;
777799
}
778-
double bri = cachedHSB.getBrightness().doubleValue() * 255.0 / 100.0;
800+
double bri = hsb.getBrightness().doubleValue() * 255.0 / 100.0;
779801
double cool = bri * ratio;
780802
double warm = bri - cool;
781803
return new double[] { 0.0, 0.0, 0.0, cool, warm };
@@ -794,9 +816,9 @@ public synchronized double[] getRGBx() throws IllegalStateException {
794816
*/
795817
PercentType[] rgbP = ColorUtil.hsbToRgbPercent(hsb);
796818
double[] rgb = Arrays.stream(rgbP).mapToDouble(p -> p.doubleValue() * 255.0 / 100.0).toArray();
797-
if (RgbDataType.RGB_W == rgbDataType) {
819+
if (RGB_W_TYPES.contains(rgbDataType)) {
798820
return new double[] { rgb[0], rgb[1], rgb[2], 0 };
799-
} else if (RgbDataType.RGB_C_W == rgbDataType) {
821+
} else if (RGB_C_W_TYPES.contains(rgbDataType)) {
800822
return new double[] { rgb[0], rgb[1], rgb[2], 0, 0 };
801823
}
802824
return rgb;
@@ -806,7 +828,7 @@ public synchronized double[] getRGBx() throws IllegalStateException {
806828
* In combined mode the RGB and white values are all determined by the HSB values.
807829
*/
808830
if (LedOperatingMode.COMBINED == ledOperatingMode) {
809-
if (RgbDataType.RGB_C_W == rgbDataType) {
831+
if (RGB_C_W_TYPES.contains(rgbDataType)) {
810832
/*
811833
* RGBCW - convert HSB to RGB, normalize it, then convert to RGBCW, then scale to [0..255]
812834
*/
@@ -815,7 +837,7 @@ public synchronized double[] getRGBx() throws IllegalStateException {
815837
double[] rgbcw = RgbcwMath.rgb2rgbcw(rgb, coolWhiteLed.getProfile(), warmWhiteLed.getProfile());
816838
rgbcw = Arrays.stream(rgbcw).map(d -> Math.round(d * 255 * 10) / 10).toArray(); // // round to 1
817839
return rgbcw;
818-
} else if (RgbDataType.RGB_W == rgbDataType) {
840+
} else if (RGB_W_TYPES.contains(rgbDataType)) {
819841
/*
820842
* RGBW - convert HSB to RGBW, then scale to [0..255]
821843
*/
@@ -1031,15 +1053,15 @@ public synchronized void setOnOff(boolean on) {
10311053
* on the value of {@link #rgbDataType} the brightness may or may not change as follows:
10321054
*
10331055
* <ul>
1034-
* <li>'RGB_NO_BRIGHTNESS' both [255,0,0] and [127.5,0,0] change the color to RED without a change in brightness.
1035-
* In other words the values only relate to the 'HS' part of the {@link HSBType} state. Note: this means that in
1036-
* this case a round trip of 'setRGBx()' followed by 'getRGBx()' will NOT necessarily contain identical values,
1037-
* although the RGB ratios will certainly be the same.</li>
1056+
* <li>'RGB_NO_BRIGHTNESS', 'RGB_W_NO_BRIGHTNESS', 'RGB_C_W_NO_BRIGHTNESS': The set value does not affect the
1057+
* current brightness. In other words the values only relate to the 'HS' part of the {@link HSBType} state. Note:
1058+
* this means that in this case a round trip of 'setRGBx()' followed by 'getRGBx()' will NOT necessarily contain
1059+
* identical values, although the RGB ratios will certainly be the same.</li>
10381060
*
10391061
* <li>All other values of {@link #rgbDataType}: both [255,0,0] and [127.5,0,0] change the color to RED and the
10401062
* former changes the brightness to 100 percent, whereas the latter changes it to 50 percent. In other words the
10411063
* values relate to all the 'HSB' parts of the {@link HSBType} state.</li>
1042-
* <ul>
1064+
* </ul>
10431065
*
10441066
* @param rgbxParameter an array of double representing RGB or RGBW values in range [0.0..255.0]
10451067
* @throws IllegalArgumentException if the array length is not 3, 4, or 5 depending on the light's capabilities,
@@ -1049,8 +1071,8 @@ public synchronized void setRGBx(double[] rgbxParameter) throws IllegalArgumentE
10491071
if (rgbxParameter.length > 5) {
10501072
throw new IllegalArgumentException("Too many arguments in RGBx array");
10511073
}
1052-
if (rgbxParameter.length < 3 || (RgbDataType.RGB_W == rgbDataType && rgbxParameter.length < 4)
1053-
|| (RgbDataType.RGB_C_W == rgbDataType && rgbxParameter.length < 5)) {
1074+
if (rgbxParameter.length < 3 || (RGB_W_TYPES.contains(rgbDataType) && rgbxParameter.length < 4)
1075+
|| (RGB_C_W_TYPES.contains(rgbDataType) && rgbxParameter.length < 5)) {
10541076
throw new IllegalArgumentException("Too few arguments in RGBx array");
10551077
}
10561078
if (rgbxParameter.length == 3 && ledOperatingMode != LedOperatingMode.RGB_ONLY) {
@@ -1064,10 +1086,11 @@ public synchronized void setRGBx(double[] rgbxParameter) throws IllegalArgumentE
10641086
}
10651087

10661088
HSBType hsb;
1089+
Double mirek;
1090+
PercentType oldBri = cachedHSB.getBrightness();
10671091
switch (ledOperatingMode) {
10681092
case WHITE_ONLY:
10691093
double white;
1070-
double mirek;
10711094
if (rgbxParameter.length == 5) {
10721095
/*
10731096
* We have both a C and a W channel so we create a pure white whose brightness
@@ -1114,7 +1137,7 @@ public synchronized void setRGBx(double[] rgbxParameter) throws IllegalArgumentE
11141137

11151138
case COMBINED:
11161139
double[] rgbx;
1117-
if (RgbDataType.RGB_C_W == rgbDataType) {
1140+
if (RGB_C_W_TYPES.contains(rgbDataType)) {
11181141
// RGBCW - normalize, convert to RGB, then scale back to [0..255]
11191142
rgbx = Arrays.stream(rgbxParameter).map(d -> d / 255.0).toArray();
11201143
rgbx = RgbcwMath.rgbcw2rgb(rgbx, coolWhiteLed.getProfile(), warmWhiteLed.getProfile());
@@ -1127,17 +1150,20 @@ public synchronized void setRGBx(double[] rgbxParameter) throws IllegalArgumentE
11271150
hsb = ColorUtil.rgbToHsb(Arrays.stream(rgbx).map(d -> d * 100.0 / 255.0)
11281151
.mapToObj(d -> zPercentTypeFrom(d)).toArray(PercentType[]::new));
11291152

1130-
if (RgbDataType.RGB_NO_BRIGHTNESS == rgbDataType) {
1131-
hsb = new HSBType(hsb.getHue(), hsb.getSaturation(), cachedHSB.getBrightness());
1132-
}
1153+
mirek = zMirekFrom(hsb);
11331154
break;
11341155

11351156
default:
11361157
return; // safe coding but will never happen
11371158
}
11381159

1139-
cachedHSB = hsb;
1140-
cachedMirek = zMirekFrom(hsb);
1160+
if (NO_BRIGHTNESS_TYPES.contains(rgbDataType)) {
1161+
cachedHSB = new HSBType(hsb.getHue(), hsb.getSaturation(), oldBri);
1162+
} else {
1163+
cachedHSB = hsb;
1164+
zHandleBrightness(hsb.getBrightness()); // refresh cached brightness and on/off state
1165+
}
1166+
cachedMirek = mirek;
11411167
}
11421168

11431169
/**
@@ -1209,12 +1235,12 @@ public synchronized LightModel copy() {
12091235
* @param brightness the brightness {@link PercentType} to set.
12101236
*/
12111237
private void zHandleBrightness(PercentType brightness) {
1212-
if (brightness.doubleValue() >= minimumOnBrightness) {
1238+
if (brightness.doubleValue() > minimumOnBrightness) {
12131239
cachedBrightness = brightness;
12141240
cachedHSB = new HSBType(cachedHSB.getHue(), cachedHSB.getSaturation(), brightness);
12151241
cachedOnOff = OnOffType.ON;
12161242
} else {
1217-
if (OnOffType.ON == cachedOnOff && cachedHSB.getBrightness().doubleValue() >= minimumOnBrightness) {
1243+
if (OnOffType.ON == cachedOnOff && cachedHSB.getBrightness().doubleValue() > minimumOnBrightness) {
12181244
cachedBrightness = cachedHSB.getBrightness(); // cache the last 'ON' state brightness
12191245
}
12201246
cachedHSB = new HSBType(cachedHSB.getHue(), cachedHSB.getSaturation(), PercentType.ZERO);

0 commit comments

Comments
 (0)