]> git.basschouten.com Git - openhab-addons.git/blob
5c89fc6e4dd4ce885fa3afd7de51592cd34748c5
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.exception.UnsupportedComponentException;
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     TextValue colorModeValue;
75
76     public JSONSchemaLight(ComponentFactory.ComponentConfiguration builder) {
77         super(builder);
78         colorModeValue = new TextValue();
79     }
80
81     @Override
82     protected void buildChannels() {
83         List<LightColorMode> supportedColorModes = channelConfiguration.supportedColorModes;
84         if (supportedColorModes != null && supportedColorModes.contains(LightColorMode.COLOR_MODE_COLOR_TEMP)) {
85             colorModeValue = new TextValue(
86                     supportedColorModes.stream().map(LightColorMode::serializedName).toArray(String[]::new));
87             buildChannel(COLOR_MODE_CHANNEL_ID, colorModeValue, "Color Mode", this).isAdvanced(true).build();
88         }
89
90         if (channelConfiguration.colorMode) {
91             if (supportedColorModes == null || channelConfiguration.supportedColorModes.isEmpty()) {
92                 throw new UnsupportedComponentException("JSON schema light with color modes '" + getHaID()
93                         + "' does not define supported_color_modes!");
94             }
95
96             if (LightColorMode.hasColorChannel(supportedColorModes)) {
97                 hasColorChannel = true;
98             }
99
100             if (supportedColorModes.contains(LightColorMode.COLOR_MODE_COLOR_TEMP)) {
101                 buildChannel(COLOR_TEMP_CHANNEL_ID, colorTempValue, "Color Temperature", this)
102                         .commandTopic(DUMMY_TOPIC, true, 1).commandFilter(command -> handleColorTempCommand(command))
103                         .build();
104             }
105         }
106
107         if (hasColorChannel) {
108             buildChannel(COLOR_CHANNEL_ID, colorValue, "Color", this).commandTopic(DUMMY_TOPIC, true, 1)
109                     .commandFilter(this::handleCommand).build();
110         } else if (channelConfiguration.brightness) {
111             brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, brightnessValue, "Brightness", this)
112                     .commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
113         } else {
114             onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, onOffValue, "On/Off State", this)
115                     .commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
116         }
117     }
118
119     @Override
120     protected void publishState(HSBType state) {
121         JSONState json = new JSONState();
122
123         logger.trace("Publishing new state {} of light {} to MQTT.", state, getName());
124         if (state.getBrightness().equals(PercentType.ZERO)) {
125             json.state = "OFF";
126         } else {
127             json.state = "ON";
128             if (channelConfiguration.brightness || (channelConfiguration.supportedColorModes != null
129                     && (channelConfiguration.supportedColorModes.contains(LightColorMode.COLOR_MODE_HS)
130                             || channelConfiguration.supportedColorModes.contains(LightColorMode.COLOR_MODE_XY)))) {
131                 json.brightness = state.getBrightness().toBigDecimal()
132                         .multiply(new BigDecimal(channelConfiguration.brightnessScale))
133                         .divide(new BigDecimal(100), MathContext.DECIMAL128).intValue();
134             }
135
136             if (hasColorChannel) {
137                 json.color = new JSONState.Color();
138                 if (channelConfiguration.supportedColorModes.contains(LightColorMode.COLOR_MODE_HS)) {
139                     json.color.h = state.getHue().toBigDecimal();
140                     json.color.s = state.getSaturation().toBigDecimal().divide(BIG_DECIMAL_HUNDRED);
141                 } else if (LightColorMode.hasRGB(Objects.requireNonNull(channelConfiguration.supportedColorModes))) {
142                     var rgb = state.toRGB();
143                     json.color.r = rgb[0].toBigDecimal().multiply(SCALE_FACTOR).intValue();
144                     json.color.g = rgb[1].toBigDecimal().multiply(SCALE_FACTOR).intValue();
145                     json.color.b = rgb[2].toBigDecimal().multiply(SCALE_FACTOR).intValue();
146                 } else { // if (channelConfiguration.supportedColorModes.contains(COLOR_MODE_XY))
147                     var xy = state.toXY();
148                     json.color.x = xy[0].toBigDecimal().divide(BIG_DECIMAL_HUNDRED);
149                     json.color.y = xy[1].toBigDecimal().divide(BIG_DECIMAL_HUNDRED);
150                 }
151             }
152         }
153
154         String command = getGson().toJson(json);
155         logger.debug("Publishing new state '{}' of light {} to MQTT.", command, getName());
156         rawChannel.getState().publishValue(new StringType(command));
157     }
158
159     @Override
160     protected boolean handleCommand(Command command) {
161         JSONState json = new JSONState();
162         if (command.getClass().equals(OnOffType.class)) {
163             json.state = command.toString();
164         } else if (command.getClass().equals(PercentType.class)) {
165             if (command.equals(PercentType.ZERO)) {
166                 json.state = "OFF";
167             } else {
168                 json.state = "ON";
169                 if (channelConfiguration.brightness) {
170                     json.brightness = ((PercentType) command).toBigDecimal()
171                             .multiply(new BigDecimal(channelConfiguration.brightnessScale))
172                             .divide(new BigDecimal(100), MathContext.DECIMAL128).intValue();
173                 }
174             }
175         } else {
176             return super.handleCommand(command);
177         }
178
179         String jsonCommand = getGson().toJson(json);
180         logger.debug("Publishing new state '{}' of light {} to MQTT.", jsonCommand, getName());
181         rawChannel.getState().publishValue(new StringType(jsonCommand));
182         return false;
183     }
184
185     private boolean handleColorTempCommand(Command command) {
186         JSONState json = new JSONState();
187
188         if (command instanceof DecimalType) {
189             command = new QuantityType<>(((DecimalType) command).toBigDecimal(), Units.MIRED);
190         }
191         if (command instanceof QuantityType) {
192             QuantityType<?> mireds = ((QuantityType<?>) command).toInvertibleUnit(Units.MIRED);
193             if (mireds == null) {
194                 logger.warn("Unable to convert {} to mireds", command);
195                 return false;
196             }
197             json.state = "ON";
198             json.colorTemp = mireds.toBigDecimal().intValue();
199         } else {
200             return false;
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     @Override
210     public void updateChannelState(ChannelUID channel, State state) {
211         ChannelStateUpdateListener listener = this.channelStateUpdateListener;
212
213         @Nullable
214         JSONState jsonState;
215         try {
216             jsonState = getGson().fromJson(state.toString(), JSONState.class);
217
218             if (jsonState == null) {
219                 logger.warn("JSON light state for '{}' is empty.", getHaID());
220                 return;
221             }
222         } catch (JsonSyntaxException e) {
223             logger.warn("Cannot parse JSON light state '{}' for '{}'.", state, getHaID());
224             return;
225         }
226
227         if (jsonState.state != null) {
228             onOffValue.update(onOffValue.parseCommand(new StringType(jsonState.state)));
229             if (brightnessValue.getChannelState() instanceof UnDefType) {
230                 brightnessValue.update(brightnessValue.parseCommand((OnOffType) onOffValue.getChannelState()));
231             }
232             if (colorValue.getChannelState() instanceof UnDefType) {
233                 colorValue.update(colorValue.parseCommand((OnOffType) onOffValue.getChannelState()));
234             }
235         }
236
237         if (jsonState.brightness != null) {
238             brightnessValue.update(
239                     brightnessValue.parseCommand(new DecimalType(Objects.requireNonNull(jsonState.brightness))));
240             if (colorValue.getChannelState() instanceof HSBType) {
241                 HSBType color = (HSBType) colorValue.getChannelState();
242                 colorValue.update(new HSBType(color.getHue(), color.getSaturation(),
243                         (PercentType) brightnessValue.getChannelState()));
244             } else {
245                 colorValue.update(new HSBType(DecimalType.ZERO, PercentType.ZERO,
246                         (PercentType) brightnessValue.getChannelState()));
247             }
248         }
249
250         if (jsonState.colorTemp != null) {
251             colorTempValue.update(new QuantityType(Objects.requireNonNull(jsonState.colorTemp), Units.MIRED));
252             listener.updateChannelState(new ChannelUID(getGroupUID(), COLOR_TEMP_CHANNEL_ID),
253                     colorTempValue.getChannelState());
254
255             colorModeValue.update(new StringType(LightColorMode.COLOR_MODE_COLOR_TEMP.serializedName()));
256         }
257
258         if (jsonState.color != null) {
259             PercentType brightness = brightnessValue.getChannelState() instanceof PercentType
260                     ? (PercentType) brightnessValue.getChannelState()
261                     : PercentType.HUNDRED;
262             // This corresponds to "deprecated" color mode handling, since we're not checking which color
263             // mode is currently active.
264             // HS is highest priority, then XY, then RGB
265             // See
266             // https://github.com/home-assistant/core/blob/4f965f0eca09f0d12ae1c98c6786054063a36b44/homeassistant/components/mqtt/light/schema_json.py#L258
267             if (jsonState.color.h != null && jsonState.color.s != null) {
268                 colorValue.update(new HSBType(new DecimalType(Objects.requireNonNull(jsonState.color.h)),
269                         new PercentType(Objects.requireNonNull(jsonState.color.s)), brightness));
270                 colorModeValue.update(new StringType(LightColorMode.COLOR_MODE_HS.serializedName()));
271             } else if (jsonState.color.x != null && jsonState.color.y != null) {
272                 HSBType newColor = HSBType.fromXY(jsonState.color.x.floatValue(), jsonState.color.y.floatValue());
273                 colorValue.update(new HSBType(newColor.getHue(), newColor.getSaturation(), brightness));
274                 colorModeValue.update(new StringType(LightColorMode.COLOR_MODE_XY.serializedName()));
275             } else if (jsonState.color.r != null && jsonState.color.g != null && jsonState.color.b != null) {
276                 colorValue.update(HSBType.fromRGB(jsonState.color.r, jsonState.color.g, jsonState.color.b));
277                 colorModeValue.update(new StringType(LightColorMode.COLOR_MODE_RGB.serializedName()));
278             }
279         }
280
281         if (jsonState.colorMode != null) {
282             colorModeValue.update(new StringType(jsonState.colorMode.serializedName()));
283         }
284
285         listener.updateChannelState(new ChannelUID(getGroupUID(), COLOR_MODE_CHANNEL_ID),
286                 colorModeValue.getChannelState());
287
288         if (hasColorChannel) {
289             listener.updateChannelState(buildChannelUID(COLOR_CHANNEL_ID), colorValue.getChannelState());
290         } else if (brightnessChannel != null) {
291             listener.updateChannelState(buildChannelUID(BRIGHTNESS_CHANNEL_ID), brightnessValue.getChannelState());
292         } else {
293             listener.updateChannelState(buildChannelUID(ON_OFF_CHANNEL_ID), onOffValue.getChannelState());
294         }
295     }
296 }