2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.mqtt.homeassistant.internal.component;
15 import java.math.BigDecimal;
16 import java.math.MathContext;
17 import java.util.List;
18 import java.util.Objects;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
23 import org.openhab.binding.mqtt.generic.values.TextValue;
24 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
25 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannelType;
26 import org.openhab.core.library.types.DecimalType;
27 import org.openhab.core.library.types.HSBType;
28 import org.openhab.core.library.types.OnOffType;
29 import org.openhab.core.library.types.PercentType;
30 import org.openhab.core.library.types.QuantityType;
31 import org.openhab.core.library.types.StringType;
32 import org.openhab.core.library.unit.Units;
33 import org.openhab.core.thing.ChannelUID;
34 import org.openhab.core.types.Command;
35 import org.openhab.core.types.State;
36 import org.openhab.core.types.UnDefType;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
40 import com.google.gson.JsonSyntaxException;
41 import com.google.gson.annotations.SerializedName;
44 * A MQTT light, following the https://www.home-assistant.io/components/light.mqtt/ specification.
46 * Specifically, the JSON schema. All channels are synthetic, and wrap the single internal raw
49 * @author Cody Cutrer - Initial contribution
52 public class JSONSchemaLight extends AbstractRawSchemaLight {
53 private static final BigDecimal SCALE_FACTOR = new BigDecimal("2.55"); // string to not lose precision
54 private static final BigDecimal BIG_DECIMAL_HUNDRED = new BigDecimal(100);
56 private final Logger logger = LoggerFactory.getLogger(JSONSchemaLight.class);
58 private static class JSONState {
59 protected static class Color {
60 protected @Nullable Integer r, g, b, c, w;
61 protected @Nullable BigDecimal x, y, h, s;
64 protected @Nullable String state;
65 protected @Nullable Integer brightness;
66 @SerializedName("color_mode")
67 protected @Nullable LightColorMode colorMode;
68 @SerializedName("color_temp")
69 protected @Nullable Integer colorTemp;
70 protected @Nullable Color color;
71 protected @Nullable String effect;
72 protected @Nullable Integer transition;
75 public JSONSchemaLight(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
76 super(builder, newStyleChannels);
80 protected void buildChannels() {
81 boolean hasColorChannel = false;
82 List<LightColorMode> supportedColorModes = channelConfiguration.supportedColorModes;
83 if (supportedColorModes != null) {
84 if (LightColorMode.hasColorChannel(supportedColorModes)) {
85 hasColorChannel = true;
88 if (supportedColorModes.contains(LightColorMode.COLOR_MODE_COLOR_TEMP)) {
89 buildChannel(COLOR_TEMP_CHANNEL_ID, ComponentChannelType.NUMBER, colorTempValue, "Color Temperature",
90 this).commandTopic(DUMMY_TOPIC, true, 1)
91 .commandFilter(command -> handleColorTempCommand(command)).build();
93 if (hasColorChannel) {
94 colorModeValue = new TextValue(
95 supportedColorModes.stream().map(LightColorMode::serializedName).toArray(String[]::new));
96 buildChannel(COLOR_MODE_CHANNEL_ID, ComponentChannelType.STRING, colorModeValue, "Color Mode", this)
97 .isAdvanced(true).build();
103 if (hasColorChannel) {
104 colorChannel = buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this)
105 .commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
106 } else if (channelConfiguration.brightness) {
107 brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue,
108 "Brightness", this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
110 onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, ComponentChannelType.SWITCH, onOffValue, "On/Off State",
111 this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
114 if (effectValue != null) {
115 buildChannel(EFFECT_CHANNEL_ID, ComponentChannelType.STRING, Objects.requireNonNull(effectValue),
116 "Lighting Effect", this).commandTopic(DUMMY_TOPIC, true, 1)
117 .commandFilter(command -> handleEffectCommand(command)).build();
122 private boolean handleEffectCommand(Command command) {
123 if (command instanceof StringType) {
124 JSONState json = new JSONState();
126 json.effect = command.toString();
133 protected void publishState(HSBType state) {
134 JSONState json = new JSONState();
136 logger.trace("Publishing new state {} of light {} to MQTT.", state, getName());
137 if (state.getBrightness().equals(PercentType.ZERO)) {
141 if (channelConfiguration.brightness || (channelConfiguration.supportedColorModes != null
142 && (channelConfiguration.supportedColorModes.contains(LightColorMode.COLOR_MODE_HS)
143 || channelConfiguration.supportedColorModes.contains(LightColorMode.COLOR_MODE_XY)))) {
144 json.brightness = state.getBrightness().toBigDecimal()
145 .multiply(new BigDecimal(channelConfiguration.brightnessScale))
146 .divide(new BigDecimal(100), MathContext.DECIMAL128).intValue();
149 if (colorChannel != null) {
150 json.color = new JSONState.Color();
151 if (channelConfiguration.supportedColorModes.contains(LightColorMode.COLOR_MODE_HS)) {
152 json.color.h = state.getHue().toBigDecimal();
153 json.color.s = state.getSaturation().toBigDecimal().divide(BIG_DECIMAL_HUNDRED);
154 } else if (LightColorMode.hasRGB(Objects.requireNonNull(channelConfiguration.supportedColorModes))) {
155 var rgb = state.toRGB();
156 json.color.r = rgb[0].toBigDecimal().multiply(SCALE_FACTOR).intValue();
157 json.color.g = rgb[1].toBigDecimal().multiply(SCALE_FACTOR).intValue();
158 json.color.b = rgb[2].toBigDecimal().multiply(SCALE_FACTOR).intValue();
159 } else { // if (channelConfiguration.supportedColorModes.contains(COLOR_MODE_XY))
160 var xy = state.toXY();
161 json.color.x = xy[0].toBigDecimal().divide(BIG_DECIMAL_HUNDRED);
162 json.color.y = xy[1].toBigDecimal().divide(BIG_DECIMAL_HUNDRED);
170 private void publishState(JSONState json) {
171 String command = getGson().toJson(json);
172 logger.debug("Publishing new state '{}' of light {} to MQTT.", command, getName());
173 rawChannel.getState().publishValue(new StringType(command));
177 protected boolean handleCommand(Command command) {
178 JSONState json = new JSONState();
179 if (command.getClass().equals(OnOffType.class)) {
180 json.state = command.toString();
181 } else if (command.getClass().equals(PercentType.class)) {
182 if (command.equals(PercentType.ZERO)) {
186 if (channelConfiguration.brightness) {
187 json.brightness = ((PercentType) command).toBigDecimal()
188 .multiply(new BigDecimal(channelConfiguration.brightnessScale))
189 .divide(new BigDecimal(100), MathContext.DECIMAL128).intValue();
193 return super.handleCommand(command);
196 String jsonCommand = getGson().toJson(json);
197 logger.debug("Publishing new state '{}' of light {} to MQTT.", jsonCommand, getName());
198 rawChannel.getState().publishValue(new StringType(jsonCommand));
202 private boolean handleColorTempCommand(Command command) {
203 JSONState json = new JSONState();
205 if (command instanceof DecimalType) {
206 command = new QuantityType<>(((DecimalType) command).toBigDecimal(), Units.MIRED);
208 if (command instanceof QuantityType) {
209 QuantityType<?> mireds = ((QuantityType<?>) command).toInvertibleUnit(Units.MIRED);
210 if (mireds == null) {
211 logger.warn("Unable to convert {} to mireds", command);
215 json.colorTemp = mireds.toBigDecimal().intValue();
220 String jsonCommand = getGson().toJson(json);
221 logger.debug("Publishing new state '{}' of light {} to MQTT.", jsonCommand, getName());
222 rawChannel.getState().publishValue(new StringType(jsonCommand));
227 public void updateChannelState(ChannelUID channel, State state) {
228 ChannelStateUpdateListener listener = this.channelStateUpdateListener;
233 jsonState = getGson().fromJson(state.toString(), JSONState.class);
235 if (jsonState == null) {
236 logger.warn("JSON light state for '{}' is empty.", getHaID());
239 } catch (JsonSyntaxException e) {
240 logger.warn("Cannot parse JSON light state '{}' for '{}'.", state, getHaID());
244 if (effectValue != null) {
245 if (jsonState.effect != null) {
246 effectValue.update(new StringType(jsonState.effect));
247 listener.updateChannelState(buildChannelUID(EFFECT_CHANNEL_ID), effectValue.getChannelState());
249 listener.updateChannelState(buildChannelUID(EFFECT_CHANNEL_ID), UnDefType.NULL);
254 if (jsonState.state != null) {
255 onOffValue.update((State) onOffValue.parseMessage(new StringType(jsonState.state)));
256 off = onOffValue.getChannelState().equals(OnOffType.OFF);
257 if (onOffValue.getChannelState() instanceof OnOffType onOffState) {
258 if (brightnessValue.getChannelState() instanceof UnDefType) {
259 brightnessValue.update(Objects.requireNonNull(onOffState.as(PercentType.class)));
261 if (colorValue.getChannelState() instanceof UnDefType) {
262 colorValue.update(Objects.requireNonNull(onOffState.as(PercentType.class)));
267 PercentType brightness;
269 brightness = PercentType.ZERO;
270 } else if (brightnessValue.getChannelState() instanceof PercentType percentValue) {
271 brightness = percentValue;
273 brightness = PercentType.HUNDRED;
276 if (jsonState.brightness != null) {
278 brightness = (PercentType) brightnessValue
279 .parseMessage(new DecimalType(Objects.requireNonNull(jsonState.brightness)));
281 brightnessValue.update(brightness);
282 if (colorValue.getChannelState() instanceof HSBType) {
283 HSBType color = (HSBType) colorValue.getChannelState();
284 colorValue.update(new HSBType(color.getHue(), color.getSaturation(), brightness));
286 colorValue.update(new HSBType(DecimalType.ZERO, PercentType.ZERO, brightness));
290 if (jsonState.colorTemp != null) {
291 colorTempValue.update(new QuantityType(Objects.requireNonNull(jsonState.colorTemp), Units.MIRED));
292 listener.updateChannelState(buildChannelUID(COLOR_TEMP_CHANNEL_ID), colorTempValue.getChannelState());
294 colorModeValue.update(new StringType(LightColorMode.COLOR_MODE_COLOR_TEMP.serializedName()));
297 if (jsonState.color != null) {
298 // This corresponds to "deprecated" color mode handling, since we're not checking which color
299 // mode is currently active.
300 // HS is highest priority, then XY, then RGB
302 // https://github.com/home-assistant/core/blob/4f965f0eca09f0d12ae1c98c6786054063a36b44/homeassistant/components/mqtt/light/schema_json.py#L258
303 if (jsonState.color.h != null && jsonState.color.s != null) {
304 colorValue.update(new HSBType(new DecimalType(Objects.requireNonNull(jsonState.color.h)),
305 new PercentType(Objects.requireNonNull(jsonState.color.s)), brightness));
306 colorModeValue.update(new StringType(LightColorMode.COLOR_MODE_HS.serializedName()));
307 } else if (jsonState.color.x != null && jsonState.color.y != null) {
308 HSBType newColor = HSBType.fromXY(jsonState.color.x.floatValue(), jsonState.color.y.floatValue());
309 colorValue.update(new HSBType(newColor.getHue(), newColor.getSaturation(), brightness));
310 colorModeValue.update(new StringType(LightColorMode.COLOR_MODE_XY.serializedName()));
311 } else if (jsonState.color.r != null && jsonState.color.g != null && jsonState.color.b != null) {
312 colorValue.update(HSBType.fromRGB(jsonState.color.r, jsonState.color.g, jsonState.color.b));
313 colorModeValue.update(new StringType(LightColorMode.COLOR_MODE_RGB.serializedName()));
317 if (jsonState.colorMode != null) {
318 colorModeValue.update(new StringType(jsonState.colorMode.serializedName()));
321 listener.updateChannelState(buildChannelUID(COLOR_MODE_CHANNEL_ID), colorModeValue.getChannelState());
323 ComponentChannel localBrightnessChannel = brightnessChannel;
324 ComponentChannel localColorChannel = colorChannel;
325 if (localColorChannel != null) {
326 listener.updateChannelState(localColorChannel.getChannel().getUID(), colorValue.getChannelState());
327 } else if (localBrightnessChannel != null) {
328 listener.updateChannelState(localBrightnessChannel.getChannel().getUID(),
329 brightnessValue.getChannelState());
331 listener.updateChannelState(onOffChannel.getChannel().getUID(), onOffValue.getChannelState());