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