]> git.basschouten.com Git - openhab-addons.git/blob
ca016efd345d95b99dad88d316501b5bc932217d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.mqtt.homeassistant.internal.component;
14
15 import java.math.BigDecimal;
16 import java.math.MathContext;
17 import java.util.List;
18 import java.util.Objects;
19
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.ComponentChannelType;
25 import org.openhab.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException;
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;
39
40 import com.google.gson.JsonSyntaxException;
41 import com.google.gson.annotations.SerializedName;
42
43 /**
44  * A MQTT light, following the https://www.home-assistant.io/components/light.mqtt/ specification.
45  *
46  * Specifically, the JSON schema. All channels are synthetic, and wrap the single internal raw
47  * state.
48  *
49  * @author Cody Cutrer - Initial contribution
50  */
51 @NonNullByDefault
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);
55
56     private final Logger logger = LoggerFactory.getLogger(JSONSchemaLight.class);
57
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;
62         }
63
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;
73     }
74
75     TextValue colorModeValue;
76
77     public JSONSchemaLight(ComponentFactory.ComponentConfiguration builder, boolean newStyleChannels) {
78         super(builder, newStyleChannels);
79         colorModeValue = new TextValue();
80     }
81
82     @Override
83     protected void buildChannels() {
84         List<LightColorMode> supportedColorModes = channelConfiguration.supportedColorModes;
85         if (supportedColorModes != null && supportedColorModes.contains(LightColorMode.COLOR_MODE_COLOR_TEMP)) {
86             colorModeValue = new TextValue(
87                     supportedColorModes.stream().map(LightColorMode::serializedName).toArray(String[]::new));
88             buildChannel(COLOR_MODE_CHANNEL_ID, ComponentChannelType.STRING, colorModeValue, "Color Mode", this)
89                     .isAdvanced(true).build();
90         }
91
92         if (channelConfiguration.colorMode) {
93             if (supportedColorModes == null || channelConfiguration.supportedColorModes.isEmpty()) {
94                 throw new UnsupportedComponentException("JSON schema light with color modes '" + getHaID()
95                         + "' does not define supported_color_modes!");
96             }
97
98             if (LightColorMode.hasColorChannel(supportedColorModes)) {
99                 hasColorChannel = true;
100             }
101
102             if (supportedColorModes.contains(LightColorMode.COLOR_MODE_COLOR_TEMP)) {
103                 buildChannel(COLOR_TEMP_CHANNEL_ID, ComponentChannelType.NUMBER, colorTempValue, "Color Temperature",
104                         this).commandTopic(DUMMY_TOPIC, true, 1)
105                         .commandFilter(command -> handleColorTempCommand(command)).build();
106             }
107         }
108
109         if (hasColorChannel) {
110             buildChannel(COLOR_CHANNEL_ID, ComponentChannelType.COLOR, colorValue, "Color", this)
111                     .commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
112         } else if (channelConfiguration.brightness) {
113             brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, ComponentChannelType.DIMMER, brightnessValue,
114                     "Brightness", this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
115         } else {
116             onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, ComponentChannelType.SWITCH, onOffValue, "On/Off State",
117                     this).commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
118         }
119
120         if (effectValue != null) {
121             buildChannel(EFFECT_CHANNEL_ID, ComponentChannelType.STRING, Objects.requireNonNull(effectValue),
122                     "Lighting Effect", this).commandTopic(DUMMY_TOPIC, true, 1)
123                     .commandFilter(command -> handleEffectCommand(command)).build();
124
125         }
126     }
127
128     private boolean handleEffectCommand(Command command) {
129         if (command instanceof StringType) {
130             JSONState json = new JSONState();
131             json.state = "ON";
132             json.effect = command.toString();
133             publishState(json);
134         }
135         return false;
136     }
137
138     @Override
139     protected void publishState(HSBType state) {
140         JSONState json = new JSONState();
141
142         logger.trace("Publishing new state {} of light {} to MQTT.", state, getName());
143         if (state.getBrightness().equals(PercentType.ZERO)) {
144             json.state = "OFF";
145         } else {
146             json.state = "ON";
147             if (channelConfiguration.brightness || (channelConfiguration.supportedColorModes != null
148                     && (channelConfiguration.supportedColorModes.contains(LightColorMode.COLOR_MODE_HS)
149                             || channelConfiguration.supportedColorModes.contains(LightColorMode.COLOR_MODE_XY)))) {
150                 json.brightness = state.getBrightness().toBigDecimal()
151                         .multiply(new BigDecimal(channelConfiguration.brightnessScale))
152                         .divide(new BigDecimal(100), MathContext.DECIMAL128).intValue();
153             }
154
155             if (hasColorChannel) {
156                 json.color = new JSONState.Color();
157                 if (channelConfiguration.supportedColorModes.contains(LightColorMode.COLOR_MODE_HS)) {
158                     json.color.h = state.getHue().toBigDecimal();
159                     json.color.s = state.getSaturation().toBigDecimal().divide(BIG_DECIMAL_HUNDRED);
160                 } else if (LightColorMode.hasRGB(Objects.requireNonNull(channelConfiguration.supportedColorModes))) {
161                     var rgb = state.toRGB();
162                     json.color.r = rgb[0].toBigDecimal().multiply(SCALE_FACTOR).intValue();
163                     json.color.g = rgb[1].toBigDecimal().multiply(SCALE_FACTOR).intValue();
164                     json.color.b = rgb[2].toBigDecimal().multiply(SCALE_FACTOR).intValue();
165                 } else { // if (channelConfiguration.supportedColorModes.contains(COLOR_MODE_XY))
166                     var xy = state.toXY();
167                     json.color.x = xy[0].toBigDecimal().divide(BIG_DECIMAL_HUNDRED);
168                     json.color.y = xy[1].toBigDecimal().divide(BIG_DECIMAL_HUNDRED);
169                 }
170             }
171         }
172
173         publishState(json);
174     }
175
176     private void publishState(JSONState json) {
177         String command = getGson().toJson(json);
178         logger.debug("Publishing new state '{}' of light {} to MQTT.", command, getName());
179         rawChannel.getState().publishValue(new StringType(command));
180     }
181
182     @Override
183     protected boolean handleCommand(Command command) {
184         JSONState json = new JSONState();
185         if (command.getClass().equals(OnOffType.class)) {
186             json.state = command.toString();
187         } else if (command.getClass().equals(PercentType.class)) {
188             if (command.equals(PercentType.ZERO)) {
189                 json.state = "OFF";
190             } else {
191                 json.state = "ON";
192                 if (channelConfiguration.brightness) {
193                     json.brightness = ((PercentType) command).toBigDecimal()
194                             .multiply(new BigDecimal(channelConfiguration.brightnessScale))
195                             .divide(new BigDecimal(100), MathContext.DECIMAL128).intValue();
196                 }
197             }
198         } else {
199             return super.handleCommand(command);
200         }
201
202         String jsonCommand = getGson().toJson(json);
203         logger.debug("Publishing new state '{}' of light {} to MQTT.", jsonCommand, getName());
204         rawChannel.getState().publishValue(new StringType(jsonCommand));
205         return false;
206     }
207
208     private boolean handleColorTempCommand(Command command) {
209         JSONState json = new JSONState();
210
211         if (command instanceof DecimalType) {
212             command = new QuantityType<>(((DecimalType) command).toBigDecimal(), Units.MIRED);
213         }
214         if (command instanceof QuantityType) {
215             QuantityType<?> mireds = ((QuantityType<?>) command).toInvertibleUnit(Units.MIRED);
216             if (mireds == null) {
217                 logger.warn("Unable to convert {} to mireds", command);
218                 return false;
219             }
220             json.state = "ON";
221             json.colorTemp = mireds.toBigDecimal().intValue();
222         } else {
223             return false;
224         }
225
226         String jsonCommand = getGson().toJson(json);
227         logger.debug("Publishing new state '{}' of light {} to MQTT.", jsonCommand, getName());
228         rawChannel.getState().publishValue(new StringType(jsonCommand));
229         return false;
230     }
231
232     @Override
233     public void updateChannelState(ChannelUID channel, State state) {
234         ChannelStateUpdateListener listener = this.channelStateUpdateListener;
235
236         @Nullable
237         JSONState jsonState;
238         try {
239             jsonState = getGson().fromJson(state.toString(), JSONState.class);
240
241             if (jsonState == null) {
242                 logger.warn("JSON light state for '{}' is empty.", getHaID());
243                 return;
244             }
245         } catch (JsonSyntaxException e) {
246             logger.warn("Cannot parse JSON light state '{}' for '{}'.", state, getHaID());
247             return;
248         }
249
250         if (effectValue != null) {
251             if (jsonState.effect != null) {
252                 effectValue.update(new StringType(jsonState.effect));
253                 listener.updateChannelState(buildChannelUID(EFFECT_CHANNEL_ID), effectValue.getChannelState());
254             } else {
255                 listener.updateChannelState(buildChannelUID(EFFECT_CHANNEL_ID), UnDefType.NULL);
256             }
257         }
258
259         boolean off = false;
260         if (jsonState.state != null) {
261             onOffValue.update((State) onOffValue.parseMessage(new StringType(jsonState.state)));
262             off = onOffValue.getChannelState().equals(OnOffType.OFF);
263             if (onOffValue.getChannelState() instanceof OnOffType onOffState) {
264                 if (brightnessValue.getChannelState() instanceof UnDefType) {
265                     brightnessValue.update(Objects.requireNonNull(onOffState.as(PercentType.class)));
266                 }
267                 if (colorValue.getChannelState() instanceof UnDefType) {
268                     colorValue.update(Objects.requireNonNull(onOffState.as(PercentType.class)));
269                 }
270             }
271         }
272
273         PercentType brightness;
274         if (off) {
275             brightness = PercentType.ZERO;
276         } else if (brightnessValue.getChannelState() instanceof PercentType percentValue) {
277             brightness = percentValue;
278         } else {
279             brightness = PercentType.HUNDRED;
280         }
281
282         if (jsonState.brightness != null) {
283             if (!off) {
284                 brightness = (PercentType) brightnessValue
285                         .parseMessage(new DecimalType(Objects.requireNonNull(jsonState.brightness)));
286             }
287             brightnessValue.update(brightness);
288             if (colorValue.getChannelState() instanceof HSBType) {
289                 HSBType color = (HSBType) colorValue.getChannelState();
290                 colorValue.update(new HSBType(color.getHue(), color.getSaturation(), brightness));
291             } else {
292                 colorValue.update(new HSBType(DecimalType.ZERO, PercentType.ZERO, brightness));
293             }
294         }
295
296         if (jsonState.colorTemp != null) {
297             colorTempValue.update(new QuantityType(Objects.requireNonNull(jsonState.colorTemp), Units.MIRED));
298             listener.updateChannelState(buildChannelUID(COLOR_TEMP_CHANNEL_ID), colorTempValue.getChannelState());
299
300             colorModeValue.update(new StringType(LightColorMode.COLOR_MODE_COLOR_TEMP.serializedName()));
301         }
302
303         if (jsonState.color != null) {
304             // This corresponds to "deprecated" color mode handling, since we're not checking which color
305             // mode is currently active.
306             // HS is highest priority, then XY, then RGB
307             // See
308             // https://github.com/home-assistant/core/blob/4f965f0eca09f0d12ae1c98c6786054063a36b44/homeassistant/components/mqtt/light/schema_json.py#L258
309             if (jsonState.color.h != null && jsonState.color.s != null) {
310                 colorValue.update(new HSBType(new DecimalType(Objects.requireNonNull(jsonState.color.h)),
311                         new PercentType(Objects.requireNonNull(jsonState.color.s)), brightness));
312                 colorModeValue.update(new StringType(LightColorMode.COLOR_MODE_HS.serializedName()));
313             } else if (jsonState.color.x != null && jsonState.color.y != null) {
314                 HSBType newColor = HSBType.fromXY(jsonState.color.x.floatValue(), jsonState.color.y.floatValue());
315                 colorValue.update(new HSBType(newColor.getHue(), newColor.getSaturation(), brightness));
316                 colorModeValue.update(new StringType(LightColorMode.COLOR_MODE_XY.serializedName()));
317             } else if (jsonState.color.r != null && jsonState.color.g != null && jsonState.color.b != null) {
318                 colorValue.update(HSBType.fromRGB(jsonState.color.r, jsonState.color.g, jsonState.color.b));
319                 colorModeValue.update(new StringType(LightColorMode.COLOR_MODE_RGB.serializedName()));
320             }
321         }
322
323         if (jsonState.colorMode != null) {
324             colorModeValue.update(new StringType(jsonState.colorMode.serializedName()));
325         }
326
327         listener.updateChannelState(buildChannelUID(COLOR_MODE_CHANNEL_ID), colorModeValue.getChannelState());
328
329         if (hasColorChannel) {
330             listener.updateChannelState(buildChannelUID(COLOR_CHANNEL_ID), colorValue.getChannelState());
331         } else if (brightnessChannel != null) {
332             listener.updateChannelState(buildChannelUID(BRIGHTNESS_CHANNEL_ID), brightnessValue.getChannelState());
333         } else {
334             listener.updateChannelState(buildChannelUID(ON_OFF_CHANNEL_ID), onOffValue.getChannelState());
335         }
336     }
337 }