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