2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.mqtt.homeassistant.internal.component;
15 import java.math.BigDecimal;
16 import java.util.Objects;
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;
36 * A MQTT light, following the https://www.home-assistant.io/components/light.mqtt/ specification.
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.
41 * @author Cody Cutrer - Initial contribution
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";
52 protected @Nullable ComponentChannel hsChannel;
53 protected @Nullable ComponentChannel rgbChannel;
54 protected @Nullable ComponentChannel xyChannel;
56 public DefaultSchemaLight(ComponentFactory.ComponentConfiguration builder) {
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);
70 ComponentChannel localBrightnessChannel = null;
71 if (channelConfiguration.brightnessStateTopic != null || channelConfiguration.brightnessCommandTopic != null) {
72 localBrightnessChannel = brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, brightnessValue,
74 .stateTopic(channelConfiguration.brightnessStateTopic,
75 channelConfiguration.brightnessValueTemplate)
76 .commandTopic(channelConfiguration.brightnessCommandTopic, channelConfiguration.isRetain(),
77 channelConfiguration.getQos())
78 .withFormat("%.0f").commandFilter(this::handleBrightnessCommand).build(false);
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();
88 if (channelConfiguration.colorModeStateTopic != null) {
89 buildChannel(COLOR_MODE_CHANNEL_ID, new TextValue(), "Current color mode", this)
90 .stateTopic(channelConfiguration.colorModeStateTopic, channelConfiguration.colorModeValueTemplate)
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())
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())
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),
114 .stateTopic(channelConfiguration.rgbStateTopic, channelConfiguration.rgbValueTemplate)
115 .commandTopic(channelConfiguration.rgbCommandTopic, channelConfiguration.isRetain(),
116 channelConfiguration.getQos())
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())
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())
138 if (channelConfiguration.xyStateTopic != null || channelConfiguration.xyCommandTopic != null) {
139 hasColorChannel = true;
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())
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())
157 if (hasColorChannel) {
158 hiddenChannels.add(localOnOffChannel);
159 if (localBrightnessChannel != null) {
160 hiddenChannels.add(localBrightnessChannel);
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);
169 channels.put(ON_OFF_CHANNEL_ID, localOnOffChannel);
173 // all handle*Command methods return false if they've been handled,
174 // or true if default handling should continue
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);
185 brightnessChannel.getState().publishValue((Command) brightnessValue.getChannelState());
193 // The helper method the other commandFilters call
194 private boolean handleOnOffCommand(Command command) {
195 if (!handleRawOnOffCommand(command)) {
199 // OnOffType commands to go the regular command topic
200 if (command instanceof OnOffType) {
201 onOffChannel.getState().publishValue(command);
205 boolean needsOn = !onOffValue.getChannelState().equals(OnOffType.ON);
206 if (command.equals(PercentType.ZERO) || command.equals(HSBType.BLACK)) {
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
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);
225 private boolean handleColorCommand(Command command) {
226 if (!handleOnOffCommand(command)) {
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
233 if (channelConfiguration.brightnessCommandTopic != null) {
234 brightnessChannel.getState().publishValue(color.getBrightness());
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) {
242 // } else if (channelConfiguration.rgbwwCommandTopic != null) {
244 } else if (channelConfiguration.xyCommandTopic != null) {
245 PercentType[] xy = color.toXY();
246 // If we don't have a brightness channel, something is probably busted
248 if (channelConfiguration.brightnessCommandTopic != null) {
249 brightnessChannel.getState().publishValue(color.getBrightness());
251 String xyString = String.format("%f,%f", xy[0].doubleValue(), xy[1].doubleValue());
252 xyChannel.getState().publishValue(new StringType(xyString));
254 } else if (command instanceof PercentType) {
255 if (channelConfiguration.brightnessCommandTopic != null) {
256 brightnessChannel.getState().publishValue(command);
258 // No brightness command topic?! must be RGB only
260 State color = colorValue.getChannelState();
261 if (color instanceof UnDefType) {
262 color = HSBType.WHITE;
264 HSBType existingColor = (HSBType) color;
265 HSBType newCommand = new HSBType(existingColor.getHue(), existingColor.getSaturation(),
266 (PercentType) command);
268 handleColorCommand(newCommand);
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()
283 if (state.equals(OnOffType.ON)) {
284 colorValue.update(newOnState);
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);
293 listener.updateChannelState(channel, state);
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()));
304 colorValue.update(new HSBType(DecimalType.ZERO, PercentType.ZERO,
305 (PercentType) brightnessValue.getChannelState()));
307 listener.updateChannelState(new ChannelUID(getGroupUID(), COLOR_CHANNEL_ID),
308 colorValue.getChannelState());
310 listener.updateChannelState(channel, state);
313 case COLOR_TEMP_CHANNEL_ID:
314 case EFFECT_CHANNEL_ID:
315 // Real channels; pass through
316 listener.updateChannelState(channel, state);
320 if (brightnessValue.getChannelState() instanceof UnDefType) {
321 brightnessValue.update(PercentType.HUNDRED);
323 String[] split = state.toString().split(",");
324 if (split.length != 2) {
325 throw new IllegalArgumentException(state.toString() + " is not a valid string syntax");
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));
333 HSBType xyColor = HSBType.fromXY(x, y);
334 colorValue.update(new HSBType(xyColor.getHue(), xyColor.getSaturation(), brightness));
336 listener.updateChannelState(new ChannelUID(getGroupUID(), COLOR_CHANNEL_ID),
337 colorValue.getChannelState());
340 colorValue.update((HSBType) state);
341 listener.updateChannelState(new ChannelUID(getGroupUID(), COLOR_CHANNEL_ID),
342 colorValue.getChannelState());
344 case RGBW_CHANNEL_ID:
345 case RGBWW_CHANNEL_ID:
346 // TODO: update color value