]> git.basschouten.com Git - openhab-addons.git/blob
01e8dcb9fe02ba35264650144a1e6bac991d5f68
[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.homeassistant.internal.exception.UnsupportedComponentException;
24 import org.openhab.core.library.types.DecimalType;
25 import org.openhab.core.library.types.HSBType;
26 import org.openhab.core.library.types.OnOffType;
27 import org.openhab.core.library.types.PercentType;
28 import org.openhab.core.library.types.StringType;
29 import org.openhab.core.thing.ChannelUID;
30 import org.openhab.core.types.Command;
31 import org.openhab.core.types.State;
32 import org.openhab.core.types.UnDefType;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35
36 import com.google.gson.JsonSyntaxException;
37 import com.google.gson.annotations.SerializedName;
38
39 /**
40  * A MQTT light, following the https://www.home-assistant.io/components/light.mqtt/ specification.
41  *
42  * Specifically, the JSON schema. All channels are synthetic, and wrap the single internal raw
43  * state.
44  *
45  * @author Cody Cutrer - Initial contribution
46  */
47 @NonNullByDefault
48 public class JSONSchemaLight extends AbstractRawSchemaLight {
49     private static final BigDecimal SCALE_FACTOR = new BigDecimal("2.55"); // string to not lose precision
50
51     private final Logger logger = LoggerFactory.getLogger(JSONSchemaLight.class);
52
53     private static class JSONState {
54         protected static class Color {
55             protected @Nullable Integer r, g, b, c, w;
56             protected @Nullable BigDecimal x, y, h, s;
57         }
58
59         protected @Nullable String state;
60         protected @Nullable Integer brightness;
61         @SerializedName("color_mode")
62         protected @Nullable LightColorMode colorMode;
63         @SerializedName("color_temp")
64         protected @Nullable Integer colorTemp;
65         protected @Nullable Color color;
66         protected @Nullable String effect;
67         protected @Nullable Integer transition;
68     }
69
70     public JSONSchemaLight(ComponentFactory.ComponentConfiguration builder) {
71         super(builder);
72     }
73
74     @Override
75     protected void buildChannels() {
76         if (channelConfiguration.colorMode) {
77             List<LightColorMode> supportedColorModes = channelConfiguration.supportedColorModes;
78             if (supportedColorModes == null || channelConfiguration.supportedColorModes.isEmpty()) {
79                 throw new UnsupportedComponentException("JSON schema light with color modes '" + getHaID()
80                         + "' does not define supported_color_modes!");
81             }
82
83             if (LightColorMode.hasColorChannel(supportedColorModes)) {
84                 hasColorChannel = true;
85             }
86         }
87
88         if (hasColorChannel) {
89             buildChannel(COLOR_CHANNEL_ID, colorValue, "Color", this).commandTopic(DUMMY_TOPIC, true, 1)
90                     .commandFilter(this::handleCommand).build();
91         } else if (channelConfiguration.brightness) {
92             brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, brightnessValue, "Brightness", this)
93                     .commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
94         } else {
95             onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, onOffValue, "On/Off State", this)
96                     .commandTopic(DUMMY_TOPIC, true, 1).commandFilter(this::handleCommand).build();
97         }
98     }
99
100     @Override
101     protected void publishState(HSBType state) {
102         JSONState json = new JSONState();
103
104         logger.trace("Publishing new state {} of light {} to MQTT.", state, getName());
105         if (state.getBrightness().equals(PercentType.ZERO)) {
106             json.state = "OFF";
107         } else {
108             json.state = "ON";
109             if (channelConfiguration.brightness || (channelConfiguration.supportedColorModes != null
110                     && (channelConfiguration.supportedColorModes.contains(LightColorMode.COLOR_MODE_HS)
111                             || channelConfiguration.supportedColorModes.contains(LightColorMode.COLOR_MODE_XY)))) {
112                 json.brightness = state.getBrightness().toBigDecimal()
113                         .multiply(new BigDecimal(channelConfiguration.brightnessScale))
114                         .divide(new BigDecimal(100), MathContext.DECIMAL128).intValue();
115             }
116
117             if (hasColorChannel) {
118                 json.color = new JSONState.Color();
119                 if (channelConfiguration.supportedColorModes.contains(LightColorMode.COLOR_MODE_HS)) {
120                     json.color.h = state.getHue().toBigDecimal();
121                     json.color.s = state.getSaturation().toBigDecimal();
122                 } else if (LightColorMode.hasRGB(Objects.requireNonNull(channelConfiguration.supportedColorModes))) {
123                     var rgb = state.toRGB();
124                     json.color.r = rgb[0].toBigDecimal().multiply(SCALE_FACTOR).intValue();
125                     json.color.g = rgb[1].toBigDecimal().multiply(SCALE_FACTOR).intValue();
126                     json.color.b = rgb[2].toBigDecimal().multiply(SCALE_FACTOR).intValue();
127                 } else { // if (channelConfiguration.supportedColorModes.contains(COLOR_MODE_XY))
128                     var xy = state.toXY();
129                     json.color.x = xy[0].toBigDecimal();
130                     json.color.y = xy[1].toBigDecimal();
131                 }
132             }
133         }
134
135         String command = getGson().toJson(json);
136         logger.debug("Publishing new state '{}' of light {} to MQTT.", command, getName());
137         rawChannel.getState().publishValue(new StringType(command));
138     }
139
140     @Override
141     protected boolean handleCommand(Command command) {
142         JSONState json = new JSONState();
143         if (command.getClass().equals(OnOffType.class)) {
144             json.state = command.toString();
145         } else if (command.getClass().equals(PercentType.class)) {
146             if (command.equals(PercentType.ZERO)) {
147                 json.state = "OFF";
148             } else {
149                 json.state = "ON";
150                 if (channelConfiguration.brightness) {
151                     json.brightness = ((PercentType) command).toBigDecimal()
152                             .multiply(new BigDecimal(channelConfiguration.brightnessScale))
153                             .divide(new BigDecimal(100), MathContext.DECIMAL128).intValue();
154                 }
155             }
156         } else {
157             return super.handleCommand(command);
158         }
159
160         String jsonCommand = getGson().toJson(json);
161         logger.debug("Publishing new state '{}' of light {} to MQTT.", jsonCommand, getName());
162         rawChannel.getState().publishValue(new StringType(jsonCommand));
163         return false;
164     }
165
166     @Override
167     public void updateChannelState(ChannelUID channel, State state) {
168         ChannelStateUpdateListener listener = this.channelStateUpdateListener;
169
170         @Nullable
171         JSONState jsonState;
172         try {
173             jsonState = getGson().fromJson(state.toString(), JSONState.class);
174
175             if (jsonState == null) {
176                 logger.warn("JSON light state for '{}' is empty.", getHaID());
177                 return;
178             }
179         } catch (JsonSyntaxException e) {
180             logger.warn("Cannot parse JSON light state '{}' for '{}'.", state, getHaID());
181             return;
182         }
183
184         if (jsonState.state != null) {
185             onOffValue.update(onOffValue.parseCommand(new StringType(jsonState.state)));
186             if (brightnessValue.getChannelState() instanceof UnDefType) {
187                 brightnessValue.update(brightnessValue.parseCommand((OnOffType) onOffValue.getChannelState()));
188             }
189             if (colorValue.getChannelState() instanceof UnDefType) {
190                 colorValue.update(colorValue.parseCommand((OnOffType) onOffValue.getChannelState()));
191             }
192         }
193
194         if (jsonState.brightness != null) {
195             brightnessValue.update(
196                     brightnessValue.parseCommand(new DecimalType(Objects.requireNonNull(jsonState.brightness))));
197             if (colorValue.getChannelState() instanceof HSBType) {
198                 HSBType color = (HSBType) colorValue.getChannelState();
199                 colorValue.update(new HSBType(color.getHue(), color.getSaturation(),
200                         (PercentType) brightnessValue.getChannelState()));
201             } else {
202                 colorValue.update(new HSBType(DecimalType.ZERO, PercentType.ZERO,
203                         (PercentType) brightnessValue.getChannelState()));
204             }
205         }
206
207         if (jsonState.color != null) {
208             PercentType brightness = brightnessValue.getChannelState() instanceof PercentType
209                     ? (PercentType) brightnessValue.getChannelState()
210                     : PercentType.HUNDRED;
211             // This corresponds to "deprecated" color mode handling, since we're not checking which color
212             // mode is currently active.
213             // HS is highest priority, then XY, then RGB
214             // See
215             // https://github.com/home-assistant/core/blob/4f965f0eca09f0d12ae1c98c6786054063a36b44/homeassistant/components/mqtt/light/schema_json.py#L258
216             if (jsonState.color.h != null && jsonState.color.s != null) {
217                 colorValue.update(new HSBType(new DecimalType(Objects.requireNonNull(jsonState.color.h)),
218                         new PercentType(Objects.requireNonNull(jsonState.color.s)), brightness));
219             } else if (jsonState.color.x != null && jsonState.color.y != null) {
220                 HSBType newColor = HSBType.fromXY(jsonState.color.x.floatValue(), jsonState.color.y.floatValue());
221                 colorValue.update(new HSBType(newColor.getHue(), newColor.getSaturation(), brightness));
222             } else if (jsonState.color.r != null && jsonState.color.g != null && jsonState.color.b != null) {
223                 colorValue.update(HSBType.fromRGB(jsonState.color.r, jsonState.color.g, jsonState.color.b));
224             }
225         }
226
227         if (hasColorChannel) {
228             listener.updateChannelState(new ChannelUID(getGroupUID(), COLOR_CHANNEL_ID), colorValue.getChannelState());
229         } else if (brightnessChannel != null) {
230             listener.updateChannelState(new ChannelUID(getGroupUID(), BRIGHTNESS_CHANNEL_ID),
231                     brightnessValue.getChannelState());
232         } else {
233             listener.updateChannelState(new ChannelUID(getGroupUID(), ON_OFF_CHANNEL_ID), onOffValue.getChannelState());
234         }
235     }
236 }