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