From 4cbae6f111c3fcdc28e9dcbfd4098af88e6847dc Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Fri, 11 Oct 2024 06:56:17 -0600 Subject: [PATCH] [mqtt.homeassistant] implement non-deprecated color inference for JSON Schema lights (#17529) In particular, use the color_mode attribute to tell us which color space to parse in, instead of trying to guess. Some devices will fill out attributes not-pertinent to the current color mode, and their math might be... less than optimal. Also sync color temp and color channels when the color_mode is the opposite. Signed-off-by: Cody Cutrer --- .../internal/component/JSONSchemaLight.java | 144 ++++++++++++++---- 1 file changed, 114 insertions(+), 30 deletions(-) diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/JSONSchemaLight.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/JSONSchemaLight.java index 7f5eae561a311..ee744a5031a78 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/JSONSchemaLight.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/JSONSchemaLight.java @@ -35,6 +35,7 @@ import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; +import org.openhab.core.util.ColorUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,6 +57,8 @@ public class JSONSchemaLight extends AbstractRawSchemaLight { private final Logger logger = LoggerFactory.getLogger(JSONSchemaLight.class); + private @Nullable ComponentChannel colorTempChannel; + private static class JSONState { protected static class Color { protected @Nullable Integer r, g, b, c, w; @@ -88,8 +91,8 @@ protected void buildChannels() { } if (supportedColorModes.contains(LightColorMode.COLOR_MODE_COLOR_TEMP)) { - buildChannel(COLOR_TEMP_CHANNEL_ID, ComponentChannelType.NUMBER, colorTempValue, "Color Temperature", - this).commandTopic(DUMMY_TOPIC, true, 1) + colorTempChannel = buildChannel(COLOR_TEMP_CHANNEL_ID, ComponentChannelType.NUMBER, colorTempValue, + "Color Temperature", this).commandTopic(DUMMY_TOPIC, true, 1) .commandFilter(command -> handleColorTempCommand(command)) .withAutoUpdatePolicy(autoUpdatePolicy).build(); @@ -233,6 +236,8 @@ private boolean handleColorTempCommand(Command command) { @Override public void updateChannelState(ChannelUID channel, State state) { ChannelStateUpdateListener listener = this.channelStateUpdateListener; + ComponentChannel localBrightnessChannel = brightnessChannel; + ComponentChannel localColorChannel = colorChannel; @Nullable JSONState jsonState; @@ -294,41 +299,120 @@ public void updateChannelState(ChannelUID channel, State state) { } } - if (jsonState.colorTemp != null) { - colorTempValue.update(new QuantityType(Objects.requireNonNull(jsonState.colorTemp), Units.MIRED)); - listener.updateChannelState(buildChannelUID(COLOR_TEMP_CHANNEL_ID), colorTempValue.getChannelState()); + try { + LightColorMode localColorMode = jsonState.colorMode; + if (localColorMode != null) { + colorModeValue.update(new StringType(localColorMode.serializedName())); + + switch (localColorMode) { + case COLOR_MODE_COLOR_TEMP: + Integer localColorTemp = jsonState.colorTemp; + if (localColorTemp == null) { + logger.warn("Incomplete color_temp received for {}", getHaID()); + } else { + colorTempValue + .update(new QuantityType(Objects.requireNonNull(jsonState.colorTemp), Units.MIRED)); + listener.updateChannelState(buildChannelUID(COLOR_TEMP_CHANNEL_ID), + colorTempValue.getChannelState()); + + // Populate the color channel (if there is one) to match the color temperature. + // First convert color temp to XY, then to HSB, then add in the brightness + try { + final double[] xy = ColorUtil.kelvinToXY(1000000d / localColorTemp); + HSBType color = ColorUtil.xyToHsb(xy); + color = new HSBType(color.getHue(), color.getSaturation(), brightness); + colorValue.update(color); + } catch (IndexOutOfBoundsException e) { + logger.warn("Color temperature {} cannot be converted to a color for {}", + localColorTemp, getHaID()); + } + } + break; + case COLOR_MODE_XY: + if (jsonState.color == null || jsonState.color.x == null || jsonState.color.y == null) { + logger.warn("Incomplete xy color received for {}", getHaID()); + } else { + final double[] xy = new double[] { jsonState.color.x.doubleValue(), + jsonState.color.y.doubleValue() }; + HSBType newColor = ColorUtil.xyToHsb(xy); + colorValue.update(new HSBType(newColor.getHue(), newColor.getSaturation(), brightness)); + if (colorTempChannel != null) { + double kelvin = ColorUtil.xyToKelvin(xy); + colorTempValue.update(new QuantityType(kelvin, Units.KELVIN)); + listener.updateChannelState(buildChannelUID(COLOR_TEMP_CHANNEL_ID), + colorTempValue.getChannelState()); + } + } + break; + case COLOR_MODE_HS: + if (jsonState.color == null || jsonState.color.h == null || jsonState.color.s == null) { + logger.warn("Incomplete hs color received for {}", getHaID()); + } else { + colorValue.update(new HSBType(new DecimalType(Objects.requireNonNull(jsonState.color.h)), + new PercentType(Objects.requireNonNull(jsonState.color.s)), brightness)); + } + break; + case COLOR_MODE_RGB: + case COLOR_MODE_RGBW: + case COLOR_MODE_RGBWW: + if (jsonState.color == null || jsonState.color.r == null || jsonState.color.g == null + || jsonState.color.b == null) { + logger.warn("Incomplete rgb color received for {}", getHaID()); + } else { + colorValue.update(ColorUtil + .rgbToHsb(new int[] { jsonState.color.r, jsonState.color.g, jsonState.color.b })); + } + break; + default: + break; + } - colorModeValue.update(new StringType(LightColorMode.COLOR_MODE_COLOR_TEMP.serializedName())); - } + // calculate the CCT of the color (xy was special cased above, to do a more direct calculation) + if (!localColorMode.equals(LightColorMode.COLOR_MODE_COLOR_TEMP) + && !localColorMode.equals(LightColorMode.COLOR_MODE_XY) && localColorChannel != null + && colorTempChannel != null && colorValue.getChannelState() instanceof HSBType colorState) { + final double[] xy = ColorUtil.hsbToXY(colorState); + double kelvin = ColorUtil.xyToKelvin(new double[] { xy[0], xy[1] }); + colorTempValue.update(new QuantityType(kelvin, Units.KELVIN)); + listener.updateChannelState(buildChannelUID(COLOR_TEMP_CHANNEL_ID), + colorTempValue.getChannelState()); + } - if (jsonState.color != null) { - // This corresponds to "deprecated" color mode handling, since we're not checking which color - // mode is currently active. - // HS is highest priority, then XY, then RGB - // See - // https://github.com/home-assistant/core/blob/4f965f0eca09f0d12ae1c98c6786054063a36b44/homeassistant/components/mqtt/light/schema_json.py#L258 - if (jsonState.color.h != null && jsonState.color.s != null) { - colorValue.update(new HSBType(new DecimalType(Objects.requireNonNull(jsonState.color.h)), - new PercentType(Objects.requireNonNull(jsonState.color.s)), brightness)); - colorModeValue.update(new StringType(LightColorMode.COLOR_MODE_HS.serializedName())); - } else if (jsonState.color.x != null && jsonState.color.y != null) { - HSBType newColor = HSBType.fromXY(jsonState.color.x.floatValue(), jsonState.color.y.floatValue()); - colorValue.update(new HSBType(newColor.getHue(), newColor.getSaturation(), brightness)); - colorModeValue.update(new StringType(LightColorMode.COLOR_MODE_XY.serializedName())); - } else if (jsonState.color.r != null && jsonState.color.g != null && jsonState.color.b != null) { - colorValue.update(HSBType.fromRGB(jsonState.color.r, jsonState.color.g, jsonState.color.b)); - colorModeValue.update(new StringType(LightColorMode.COLOR_MODE_RGB.serializedName())); - } - } + } else { + // "deprecated" color mode handling - color mode not specified, so we just accept what we can. See + // https://github.com/home-assistant/core/blob/4f965f0eca09f0d12ae1c98c6786054063a36b44/homeassistant/components/mqtt/light/schema_json.py#L258 + if (jsonState.colorTemp != null) { + colorTempValue.update(new QuantityType(Objects.requireNonNull(jsonState.colorTemp), Units.MIRED)); + listener.updateChannelState(buildChannelUID(COLOR_TEMP_CHANNEL_ID), + colorTempValue.getChannelState()); + + colorModeValue.update(new StringType(LightColorMode.COLOR_MODE_COLOR_TEMP.serializedName())); + } + + if (jsonState.color != null) { + if (jsonState.color.h != null && jsonState.color.s != null) { + colorValue.update(new HSBType(new DecimalType(Objects.requireNonNull(jsonState.color.h)), + new PercentType(Objects.requireNonNull(jsonState.color.s)), brightness)); + colorModeValue.update(new StringType(LightColorMode.COLOR_MODE_HS.serializedName())); + } else if (jsonState.color.x != null && jsonState.color.y != null) { + HSBType newColor = ColorUtil.xyToHsb( + new double[] { jsonState.color.x.doubleValue(), jsonState.color.y.doubleValue() }); + colorValue.update(new HSBType(newColor.getHue(), newColor.getSaturation(), brightness)); + colorModeValue.update(new StringType(LightColorMode.COLOR_MODE_XY.serializedName())); + } else if (jsonState.color.r != null && jsonState.color.g != null && jsonState.color.b != null) { + colorValue.update(ColorUtil + .rgbToHsb(new int[] { jsonState.color.r, jsonState.color.g, jsonState.color.b })); + colorModeValue.update(new StringType(LightColorMode.COLOR_MODE_RGB.serializedName())); + } - if (jsonState.colorMode != null) { - colorModeValue.update(new StringType(jsonState.colorMode.serializedName())); + } + } + } catch (IllegalArgumentException e) { + logger.warn("Invalid color value for {}", getHaID()); } listener.updateChannelState(buildChannelUID(COLOR_MODE_CHANNEL_ID), colorModeValue.getChannelState()); - ComponentChannel localBrightnessChannel = brightnessChannel; - ComponentChannel localColorChannel = colorChannel; if (localColorChannel != null) { listener.updateChannelState(localColorChannel.getChannel().getUID(), colorValue.getChannelState()); } else if (localBrightnessChannel != null) {