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