]> git.basschouten.com Git - openhab-addons.git/blob
e1b2355d0999f7fd27d41e59d208744e3e6abf37
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.util.Objects;
17
18 import org.eclipse.jdt.annotation.NonNullByDefault;
19 import org.eclipse.jdt.annotation.Nullable;
20 import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener;
21 import org.openhab.binding.mqtt.generic.mapping.ColorMode;
22 import org.openhab.binding.mqtt.generic.values.ColorValue;
23 import org.openhab.binding.mqtt.generic.values.TextValue;
24 import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel;
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.StringType;
30 import org.openhab.core.thing.ChannelUID;
31 import org.openhab.core.types.Command;
32 import org.openhab.core.types.State;
33 import org.openhab.core.types.UnDefType;
34
35 /**
36  * A MQTT light, following the https://www.home-assistant.io/components/light.mqtt/ specification.
37  *
38  * Specifically, the default schema. This class will present a single channel for color, brightness,
39  * or on/off as appropriate. Additional attributes are still exposed as dedicated channels.
40  *
41  * @author Cody Cutrer - Initial contribution
42  */
43 @NonNullByDefault
44 public class DefaultSchemaLight extends Light {
45     protected static final String HS_CHANNEL_ID = "hs";
46     protected static final String RGB_CHANNEL_ID = "rgb";
47     protected static final String RGBW_CHANNEL_ID = "rgbw";
48     protected static final String RGBWW_CHANNEL_ID = "rgbww";
49     protected static final String XY_CHANNEL_ID = "xy";
50     protected static final String WHITE_CHANNEL_ID = "white";
51
52     protected @Nullable ComponentChannel hsChannel;
53     protected @Nullable ComponentChannel rgbChannel;
54     protected @Nullable ComponentChannel xyChannel;
55
56     public DefaultSchemaLight(ComponentFactory.ComponentConfiguration builder) {
57         super(builder);
58     }
59
60     @Override
61     protected void buildChannels() {
62         ComponentChannel localOnOffChannel;
63         localOnOffChannel = onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, onOffValue, "On/Off State", this)
64                 .stateTopic(channelConfiguration.stateTopic, channelConfiguration.stateValueTemplate)
65                 .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(),
66                         channelConfiguration.getQos())
67                 .commandFilter(this::handleRawOnOffCommand).build(false);
68
69         @Nullable
70         ComponentChannel localBrightnessChannel = null;
71         if (channelConfiguration.brightnessStateTopic != null || channelConfiguration.brightnessCommandTopic != null) {
72             localBrightnessChannel = brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, brightnessValue,
73                     "Brightness", this)
74                             .stateTopic(channelConfiguration.brightnessStateTopic,
75                                     channelConfiguration.brightnessValueTemplate)
76                             .commandTopic(channelConfiguration.brightnessCommandTopic, channelConfiguration.isRetain(),
77                                     channelConfiguration.getQos())
78                             .withFormat("%.0f").commandFilter(this::handleBrightnessCommand).build(false);
79         }
80
81         if (channelConfiguration.whiteCommandTopic != null) {
82             buildChannel(WHITE_CHANNEL_ID, brightnessValue, "Go directly to white of a specific brightness", this)
83                     .commandTopic(channelConfiguration.whiteCommandTopic, channelConfiguration.isRetain(),
84                             channelConfiguration.getQos())
85                     .isAdvanced(true).build();
86         }
87
88         if (channelConfiguration.colorModeStateTopic != null) {
89             buildChannel(COLOR_MODE_CHANNEL_ID, new TextValue(), "Current color mode", this)
90                     .stateTopic(channelConfiguration.colorModeStateTopic, channelConfiguration.colorModeValueTemplate)
91                     .build();
92         }
93
94         if (channelConfiguration.colorTempStateTopic != null || channelConfiguration.colorTempCommandTopic != null) {
95             buildChannel(COLOR_TEMP_CHANNEL_ID, colorTempValue, "Color Temperature", this)
96                     .stateTopic(channelConfiguration.colorTempStateTopic, channelConfiguration.colorTempValueTemplate)
97                     .commandTopic(channelConfiguration.colorTempCommandTopic, channelConfiguration.isRetain(),
98                             channelConfiguration.getQos())
99                     .build();
100         }
101
102         if (channelConfiguration.effectStateTopic != null || channelConfiguration.effectCommandTopic != null) {
103             buildChannel(EFFECT_CHANNEL_ID, effectValue, "Lighting effect", this)
104                     .stateTopic(channelConfiguration.effectStateTopic, channelConfiguration.effectValueTemplate)
105                     .commandTopic(channelConfiguration.effectCommandTopic, channelConfiguration.isRetain(),
106                             channelConfiguration.getQos())
107                     .build();
108         }
109
110         if (channelConfiguration.rgbStateTopic != null || channelConfiguration.rgbCommandTopic != null) {
111             hasColorChannel = true;
112             hiddenChannels.add(rgbChannel = buildChannel(RGB_CHANNEL_ID, new ColorValue(ColorMode.RGB, null, null, 100),
113                     "RGB state", this)
114                             .stateTopic(channelConfiguration.rgbStateTopic, channelConfiguration.rgbValueTemplate)
115                             .commandTopic(channelConfiguration.rgbCommandTopic, channelConfiguration.isRetain(),
116                                     channelConfiguration.getQos())
117                             .build(false));
118         }
119
120         if (channelConfiguration.rgbwStateTopic != null || channelConfiguration.rgbwCommandTopic != null) {
121             hasColorChannel = true;
122             hiddenChannels.add(buildChannel(RGBW_CHANNEL_ID, new TextValue(), "RGBW state", this)
123                     .stateTopic(channelConfiguration.rgbwStateTopic, channelConfiguration.rgbwValueTemplate)
124                     .commandTopic(channelConfiguration.rgbwCommandTopic, channelConfiguration.isRetain(),
125                             channelConfiguration.getQos())
126                     .build(false));
127         }
128
129         if (channelConfiguration.rgbwwStateTopic != null || channelConfiguration.rgbwwCommandTopic != null) {
130             hasColorChannel = true;
131             hiddenChannels.add(buildChannel(RGBWW_CHANNEL_ID, new TextValue(), "RGBWW state", this)
132                     .stateTopic(channelConfiguration.rgbwwStateTopic, channelConfiguration.rgbwwValueTemplate)
133                     .commandTopic(channelConfiguration.rgbwwCommandTopic, channelConfiguration.isRetain(),
134                             channelConfiguration.getQos())
135                     .build(false));
136         }
137
138         if (channelConfiguration.xyStateTopic != null || channelConfiguration.xyCommandTopic != null) {
139             hasColorChannel = true;
140             hiddenChannels.add(
141                     xyChannel = buildChannel(XY_CHANNEL_ID, new ColorValue(ColorMode.XYY, null, null, 100), "XY State",
142                             this).stateTopic(channelConfiguration.xyStateTopic, channelConfiguration.xyValueTemplate)
143                                     .commandTopic(channelConfiguration.xyCommandTopic, channelConfiguration.isRetain(),
144                                             channelConfiguration.getQos())
145                                     .build(false));
146         }
147
148         if (channelConfiguration.hsStateTopic != null || channelConfiguration.hsCommandTopic != null) {
149             hasColorChannel = true;
150             hiddenChannels.add(this.hsChannel = buildChannel(HS_CHANNEL_ID, new TextValue(), "Hue and Saturation", this)
151                     .stateTopic(channelConfiguration.hsStateTopic, channelConfiguration.hsValueTemplate)
152                     .commandTopic(channelConfiguration.hsCommandTopic, channelConfiguration.isRetain(),
153                             channelConfiguration.getQos())
154                     .build(false));
155         }
156
157         if (hasColorChannel) {
158             hiddenChannels.add(localOnOffChannel);
159             if (localBrightnessChannel != null) {
160                 hiddenChannels.add(localBrightnessChannel);
161             }
162             buildChannel(COLOR_CHANNEL_ID, colorValue, "Color", this)
163                     .commandTopic(DUMMY_TOPIC, channelConfiguration.isRetain(), channelConfiguration.getQos())
164                     .commandFilter(this::handleColorCommand).build();
165         } else if (localBrightnessChannel != null) {
166             hiddenChannels.add(localOnOffChannel);
167             channels.put(BRIGHTNESS_CHANNEL_ID, localBrightnessChannel);
168         } else {
169             channels.put(ON_OFF_CHANNEL_ID, localOnOffChannel);
170         }
171     }
172
173     // all handle*Command methods return false if they've been handled,
174     // or true if default handling should continue
175
176     // The commandFilter for onOffChannel
177     private boolean handleRawOnOffCommand(Command command) {
178         // on_command_type of brightness is not allowed to send an actual on command
179         if (command.equals(OnOffType.ON) && channelConfiguration.onCommandType.equals(ON_COMMAND_TYPE_BRIGHTNESS)) {
180             // No prior state (or explicit off); set to 100%
181             if (brightnessValue.getChannelState() instanceof UnDefType
182                     || brightnessValue.getChannelState().equals(PercentType.ZERO)) {
183                 brightnessChannel.getState().publishValue(PercentType.HUNDRED);
184             } else {
185                 brightnessChannel.getState().publishValue((Command) brightnessValue.getChannelState());
186             }
187             return false;
188         }
189
190         return true;
191     }
192
193     // The helper method the other commandFilters call
194     private boolean handleOnOffCommand(Command command) {
195         if (!handleRawOnOffCommand(command)) {
196             return false;
197         }
198
199         // OnOffType commands to go the regular command topic
200         if (command instanceof OnOffType) {
201             onOffChannel.getState().publishValue(command);
202             return false;
203         }
204
205         boolean needsOn = !onOffValue.getChannelState().equals(OnOffType.ON);
206         if (command.equals(PercentType.ZERO) || command.equals(HSBType.BLACK)) {
207             needsOn = false;
208         }
209         if (needsOn) {
210             if (channelConfiguration.onCommandType.equals(ON_COMMAND_TYPE_FIRST)) {
211                 onOffChannel.getState().publishValue(OnOffType.ON);
212             } else if (channelConfiguration.onCommandType.equals(ON_COMMAND_TYPE_LAST)) {
213                 // TODO: schedule the ON publish for after this is sent
214             }
215         }
216         return true;
217     }
218
219     private boolean handleBrightnessCommand(Command command) {
220         // if it's OnOffType, it'll get handled by this; otherwise it'll return
221         // true and PercentType will be handled as normal
222         return handleOnOffCommand(command);
223     }
224
225     private boolean handleColorCommand(Command command) {
226         if (!handleOnOffCommand(command)) {
227             return false;
228         } else if (command instanceof HSBType) {
229             HSBType color = (HSBType) command;
230             if (channelConfiguration.hsCommandTopic != null) {
231                 // If we don't have a brightness channel, something is probably busted
232                 // but don't choke
233                 if (channelConfiguration.brightnessCommandTopic != null) {
234                     brightnessChannel.getState().publishValue(color.getBrightness());
235                 }
236                 String hs = String.format("%d,%d", color.getHue().intValue(), color.getSaturation().intValue());
237                 hsChannel.getState().publishValue(new StringType(hs));
238             } else if (channelConfiguration.rgbCommandTopic != null) {
239                 rgbChannel.getState().publishValue(command);
240                 // } else if (channelConfiguration.rgbwCommandTopic != null) {
241                 // TODO
242                 // } else if (channelConfiguration.rgbwwCommandTopic != null) {
243                 // TODO
244             } else if (channelConfiguration.xyCommandTopic != null) {
245                 PercentType[] xy = color.toXY();
246                 // If we don't have a brightness channel, something is probably busted
247                 // but don't choke
248                 if (channelConfiguration.brightnessCommandTopic != null) {
249                     brightnessChannel.getState().publishValue(color.getBrightness());
250                 }
251                 String xyString = String.format("%f,%f", xy[0].doubleValue(), xy[1].doubleValue());
252                 xyChannel.getState().publishValue(new StringType(xyString));
253             }
254         } else if (command instanceof PercentType) {
255             if (channelConfiguration.brightnessCommandTopic != null) {
256                 brightnessChannel.getState().publishValue(command);
257             } else {
258                 // No brightness command topic?! must be RGB only
259                 // so re-calculatate
260                 State color = colorValue.getChannelState();
261                 if (color instanceof UnDefType) {
262                     color = HSBType.WHITE;
263                 }
264                 HSBType existingColor = (HSBType) color;
265                 HSBType newCommand = new HSBType(existingColor.getHue(), existingColor.getSaturation(),
266                         (PercentType) command);
267                 // re-process
268                 handleColorCommand(newCommand);
269             }
270         }
271         return false;
272     }
273
274     @Override
275     public void updateChannelState(ChannelUID channel, State state) {
276         ChannelStateUpdateListener listener = this.channelStateUpdateListener;
277         switch (channel.getIdWithoutGroup()) {
278             case ON_OFF_CHANNEL_ID:
279                 if (hasColorChannel) {
280                     HSBType newOnState = colorValue.getChannelState() instanceof HSBType
281                             ? (HSBType) colorValue.getChannelState()
282                             : HSBType.WHITE;
283                     if (state.equals(OnOffType.ON)) {
284                         colorValue.update(newOnState);
285                     }
286
287                     listener.updateChannelState(new ChannelUID(getGroupUID(), COLOR_CHANNEL_ID),
288                             state.equals(OnOffType.ON) ? newOnState : HSBType.BLACK);
289                 } else if (brightnessChannel != null) {
290                     listener.updateChannelState(new ChannelUID(channel.getThingUID(), BRIGHTNESS_CHANNEL_ID),
291                             state.equals(OnOffType.ON) ? brightnessValue.getChannelState() : PercentType.ZERO);
292                 } else {
293                     listener.updateChannelState(channel, state);
294                 }
295                 return;
296             case BRIGHTNESS_CHANNEL_ID:
297                 onOffValue.update(Objects.requireNonNull(state.as(OnOffType.class)));
298                 if (hasColorChannel) {
299                     if (colorValue.getChannelState() instanceof HSBType) {
300                         HSBType hsb = (HSBType) (colorValue.getChannelState());
301                         colorValue.update(new HSBType(hsb.getHue(), hsb.getSaturation(),
302                                 (PercentType) brightnessValue.getChannelState()));
303                     } else {
304                         colorValue.update(new HSBType(DecimalType.ZERO, PercentType.ZERO,
305                                 (PercentType) brightnessValue.getChannelState()));
306                     }
307                     listener.updateChannelState(new ChannelUID(getGroupUID(), COLOR_CHANNEL_ID),
308                             colorValue.getChannelState());
309                 } else {
310                     listener.updateChannelState(channel, state);
311                 }
312                 return;
313             case COLOR_TEMP_CHANNEL_ID:
314             case EFFECT_CHANNEL_ID:
315                 // Real channels; pass through
316                 listener.updateChannelState(channel, state);
317                 return;
318             case HS_CHANNEL_ID:
319             case XY_CHANNEL_ID:
320                 if (brightnessValue.getChannelState() instanceof UnDefType) {
321                     brightnessValue.update(PercentType.HUNDRED);
322                 }
323                 String[] split = state.toString().split(",");
324                 if (split.length != 2) {
325                     throw new IllegalArgumentException(state.toString() + " is not a valid string syntax");
326                 }
327                 float x = Float.parseFloat(split[0]);
328                 float y = Float.parseFloat(split[1]);
329                 PercentType brightness = (PercentType) brightnessValue.getChannelState();
330                 if (channel.getIdWithoutGroup().equals(HS_CHANNEL_ID)) {
331                     colorValue.update(new HSBType(new DecimalType(x), new PercentType(new BigDecimal(y)), brightness));
332                 } else {
333                     HSBType xyColor = HSBType.fromXY(x, y);
334                     colorValue.update(new HSBType(xyColor.getHue(), xyColor.getSaturation(), brightness));
335                 }
336                 listener.updateChannelState(new ChannelUID(getGroupUID(), COLOR_CHANNEL_ID),
337                         colorValue.getChannelState());
338                 return;
339             case RGB_CHANNEL_ID:
340                 colorValue.update((HSBType) state);
341                 listener.updateChannelState(new ChannelUID(getGroupUID(), COLOR_CHANNEL_ID),
342                         colorValue.getChannelState());
343                 break;
344             case RGBW_CHANNEL_ID:
345             case RGBWW_CHANNEL_ID:
346                 // TODO: update color value
347                 break;
348         }
349     }
350 }