2 * Copyright (c) 2010-2023 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.dmx.internal.handler;
15 import static org.openhab.binding.dmx.internal.DmxBindingConstants.*;
17 import java.util.ArrayList;
18 import java.util.List;
20 import java.util.stream.IntStream;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.openhab.binding.dmx.internal.DmxBindingConstants.ListenerType;
24 import org.openhab.binding.dmx.internal.DmxBridgeHandler;
25 import org.openhab.binding.dmx.internal.DmxThingHandler;
26 import org.openhab.binding.dmx.internal.Util;
27 import org.openhab.binding.dmx.internal.ValueSet;
28 import org.openhab.binding.dmx.internal.action.FadeAction;
29 import org.openhab.binding.dmx.internal.config.TunableWhiteThingHandlerConfiguration;
30 import org.openhab.binding.dmx.internal.multiverse.BaseDmxChannel;
31 import org.openhab.binding.dmx.internal.multiverse.DmxChannel;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.IncreaseDecreaseType;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.PercentType;
36 import org.openhab.core.thing.Bridge;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.ThingStatusDetail;
41 import org.openhab.core.thing.ThingTypeUID;
42 import org.openhab.core.types.Command;
43 import org.openhab.core.types.RefreshType;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
48 * The {@link TunableWhiteThingHandler} is responsible for handling commands, which are
51 * @author Jan N. Klug - Initial contribution
54 public class TunableWhiteThingHandler extends DmxThingHandler {
55 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_TUNABLEWHITE);
57 private final Logger logger = LoggerFactory.getLogger(TunableWhiteThingHandler.class);
59 private final List<DmxChannel> channels = new ArrayList<>();
61 private final List<Integer> currentValues = new ArrayList<>();
62 private PercentType currentBrightness = PercentType.ZERO;
63 private PercentType currentColorTemperature = new PercentType(50);
65 private ValueSet turnOnValue = new ValueSet(0, -1, DmxChannel.MAX_VALUE);
66 private ValueSet turnOffValue = new ValueSet(0, -1, DmxChannel.MIN_VALUE);
68 private int fadeTime = 0, dimTime = 0;
70 private boolean dynamicTurnOnValue = false;
71 private boolean isDimming = false;
73 public TunableWhiteThingHandler(Thing dimmerThing) {
78 public void handleCommand(ChannelUID channelUID, Command command) {
79 logger.trace("received command {} in channel {}", command, channelUID);
80 ValueSet targetValueSet = new ValueSet(fadeTime, -1);
81 switch (channelUID.getId()) {
82 case CHANNEL_BRIGHTNESS: {
83 if (command instanceof PercentType || command instanceof DecimalType) {
84 PercentType brightness = (command instanceof PercentType) ? (PercentType) command
85 : Util.toPercentValue(((DecimalType) command).intValue());
86 logger.trace("adding fade to channels in thing {}", this.thing.getUID());
87 targetValueSet.addValue(Util.toDmxValue(
88 Util.toDmxValue(brightness) * (100 - currentColorTemperature.intValue()) / 100));
89 targetValueSet.addValue(
90 Util.toDmxValue(Util.toDmxValue(brightness) * currentColorTemperature.intValue() / 100));
91 } else if (command instanceof OnOffType) {
92 logger.trace("adding {} fade to channels in thing {}", command, this.thing.getUID());
93 if (((OnOffType) command) == OnOffType.ON) {
94 targetValueSet = turnOnValue;
96 if (dynamicTurnOnValue) {
98 for (DmxChannel channel : channels) {
99 turnOnValue.addValue(channel.getValue());
101 logger.trace("stored channel values fort next turn-on");
103 targetValueSet = turnOffValue;
105 } else if (command instanceof IncreaseDecreaseType) {
106 if (isDimming && ((IncreaseDecreaseType) command).equals(IncreaseDecreaseType.INCREASE)) {
107 logger.trace("stopping fade in thing {}", this.thing.getUID());
108 channels.forEach(DmxChannel::clearAction);
112 logger.trace("starting {} fade in thing {}", command, this.thing.getUID());
113 targetValueSet = ((IncreaseDecreaseType) command).equals(IncreaseDecreaseType.INCREASE)
116 targetValueSet.setFadeTime(dimTime);
119 } else if (command instanceof RefreshType) {
120 logger.trace("sending update on refresh to channel {}:brightness", this.thing.getUID());
121 currentValues.set(0, channels.get(0).getValue());
122 currentValues.set(1, channels.get(1).getValue());
123 updateCurrentBrightnessAndTemperature();
124 updateState(channelUID, currentBrightness);
127 logger.debug("command {} not supported in channel {}:brightness", command.getClass(),
128 this.thing.getUID());
133 case CHANNEL_BRIGHTNESS_CW:
134 if (command instanceof RefreshType) {
135 logger.trace("sending update on refresh to channel {}:brightness_cw", this.thing.getUID());
136 currentValues.set(0, channels.get(0).getValue());
137 updateState(channelUID, Util.toPercentValue(currentValues.get(0)));
140 logger.debug("command {} not supported in channel {}:brightness_cw", command.getClass(),
141 this.thing.getUID());
144 case CHANNEL_BRIGHTNESS_WW:
145 if (command instanceof RefreshType) {
146 logger.trace("sending update on refresh to channel {}:brightness_ww", this.thing.getUID());
147 currentValues.set(1, channels.get(1).getValue());
148 updateState(channelUID, Util.toPercentValue(currentValues.get(1)));
151 logger.debug("command {} not supported in channel {}:brightness_ww", command.getClass(),
152 this.thing.getUID());
155 case CHANNEL_COLOR_TEMPERATURE: {
156 if (command instanceof PercentType) {
157 PercentType colorTemperature = (PercentType) command;
158 targetValueSet.addValue(Util.toDmxValue(
159 Util.toDmxValue(currentBrightness) * (100 - colorTemperature.intValue()) / 100));
160 targetValueSet.addValue(
161 Util.toDmxValue(Util.toDmxValue(currentBrightness) * colorTemperature.intValue() / 100));
162 } else if (command instanceof RefreshType) {
163 logger.trace("sending update on refresh to channel {}:color_temperature", this.thing.getUID());
164 currentValues.set(0, channels.get(0).getValue());
165 currentValues.set(1, channels.get(1).getValue());
166 updateCurrentBrightnessAndTemperature();
167 updateState(channelUID, currentColorTemperature);
170 logger.debug("command {} not supported in channel {}:color_temperature", command.getClass(),
171 this.thing.getUID());
177 logger.debug("channel {} not supported in thing {}", channelUID.getId(), this.thing.getUID());
180 final ValueSet valueSet = targetValueSet;
181 IntStream.range(0, channels.size()).forEach(i -> {
182 channels.get(i).setChannelAction(new FadeAction(valueSet.getFadeTime(), channels.get(i).getValue(),
183 valueSet.getValue(i), valueSet.getHoldTime()));
188 public void initialize() {
189 Bridge bridge = getBridge();
190 DmxBridgeHandler bridgeHandler;
191 if (bridge == null) {
192 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "no bridge assigned");
193 dmxHandlerStatus = ThingStatusDetail.CONFIGURATION_ERROR;
196 bridgeHandler = (DmxBridgeHandler) bridge.getHandler();
197 if (bridgeHandler == null) {
198 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "no bridge handler available");
199 dmxHandlerStatus = ThingStatusDetail.CONFIGURATION_ERROR;
204 TunableWhiteThingHandlerConfiguration configuration = getConfig()
205 .as(TunableWhiteThingHandlerConfiguration.class);
206 if (configuration.dmxid.isEmpty()) {
207 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
208 "DMX channel configuration missing");
209 dmxHandlerStatus = ThingStatusDetail.CONFIGURATION_ERROR;
213 List<BaseDmxChannel> configChannels = BaseDmxChannel.fromString(configuration.dmxid,
214 bridgeHandler.getUniverseId());
215 logger.trace("found {} channels in {}", configChannels.size(), this.thing.getUID());
216 for (BaseDmxChannel channel : configChannels) {
217 channels.add(bridgeHandler.getDmxChannel(channel, this.thing));
219 } catch (IllegalArgumentException e) {
220 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
221 dmxHandlerStatus = ThingStatusDetail.CONFIGURATION_ERROR;
225 currentValues.add(DmxChannel.MIN_VALUE);
226 currentValues.add(DmxChannel.MIN_VALUE);
228 fadeTime = configuration.fadetime;
229 logger.trace("setting fadeTime to {} ms in {}", fadeTime, this.thing.getUID());
231 dimTime = configuration.dimtime;
232 logger.trace("setting dimTime to {} ms in {}", fadeTime, this.thing.getUID());
234 String turnOnValueString = String.valueOf(fadeTime) + ":" + configuration.turnonvalue + ":-1";
235 ValueSet turnOnValue = ValueSet.fromString(turnOnValueString);
236 if (turnOnValue.size() % 2 == 0) {
237 this.turnOnValue = turnOnValue;
238 logger.trace("set turnonvalue to {} in {}", turnOnValue, this.thing.getUID());
240 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "turn-on value malformed");
241 dmxHandlerStatus = ThingStatusDetail.CONFIGURATION_ERROR;
244 this.turnOnValue.setFadeTime(fadeTime);
246 dynamicTurnOnValue = configuration.dynamicturnonvalue;
248 String turnOffValueString = String.valueOf(fadeTime) + ":" + configuration.turnoffvalue + ":-1";
249 ValueSet turnOffValue = ValueSet.fromString(turnOffValueString);
250 if (turnOffValue.size() % 2 == 0) {
251 this.turnOffValue = turnOffValue;
252 logger.trace("set turnoffvalue to {} in {}", turnOffValue, this.thing.getUID());
254 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "turn-off value malformed");
255 dmxHandlerStatus = ThingStatusDetail.CONFIGURATION_ERROR;
259 this.turnOffValue.setFadeTime(fadeTime);
261 // register feedback listeners
262 channels.get(0).addListener(new ChannelUID(this.thing.getUID(), CHANNEL_BRIGHTNESS_CW), this,
264 channels.get(1).addListener(new ChannelUID(this.thing.getUID(), CHANNEL_BRIGHTNESS_WW), this,
267 if (bridge.getStatus().equals(ThingStatus.ONLINE)) {
268 updateStatus(ThingStatus.ONLINE);
269 dmxHandlerStatus = ThingStatusDetail.NONE;
271 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
276 public void dispose() {
277 if (!channels.isEmpty()) {
278 channels.get(0).removeListener(new ChannelUID(this.thing.getUID(), CHANNEL_BRIGHTNESS_CW));
279 channels.get(1).removeListener(new ChannelUID(this.thing.getUID(), CHANNEL_BRIGHTNESS_WW));
281 Bridge bridge = getBridge();
282 if (bridge != null) {
283 DmxBridgeHandler bridgeHandler = (DmxBridgeHandler) bridge.getHandler();
284 if (bridgeHandler != null) {
285 bridgeHandler.unregisterDmxChannels(this.thing);
286 logger.debug("removing {} channels from {}", channels.size(), this.thing.getUID());
290 currentValues.clear();
294 public void updateChannelValue(ChannelUID channelUID, int value) {
295 updateState(channelUID, Util.toPercentValue(value));
296 switch (channelUID.getId()) {
297 case CHANNEL_BRIGHTNESS_CW:
298 currentValues.set(0, value);
300 case CHANNEL_BRIGHTNESS_WW:
301 currentValues.set(1, value);
304 logger.debug("don't know how to handle {} in tunable white type", channelUID.getId());
308 updateCurrentBrightnessAndTemperature();
309 updateState(new ChannelUID(this.thing.getUID(), CHANNEL_BRIGHTNESS), currentBrightness);
310 updateState(new ChannelUID(this.thing.getUID(), CHANNEL_COLOR_TEMPERATURE), currentColorTemperature);
311 logger.trace("received update {} for channel {}, resulting in b={}, ct={}", value, channelUID,
312 currentBrightness, currentColorTemperature);
315 private void updateCurrentBrightnessAndTemperature() {
316 currentBrightness = Util.toPercentValue(Util.toDmxValue(currentValues.get(0) + currentValues.get(1)));
317 if (!PercentType.ZERO.equals(currentBrightness)) {
318 currentColorTemperature = new PercentType(
319 100 * currentValues.get(1) / (currentValues.get(0) + currentValues.get(1)));