2 * Copyright (c) 2010-2020 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.deconz.internal.handler;
15 import static org.openhab.binding.deconz.internal.BindingConstants.*;
16 import static org.openhab.binding.deconz.internal.Util.*;
18 import java.math.BigDecimal;
19 import java.util.HashMap;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.deconz.internal.StateDescriptionProvider;
26 import org.openhab.binding.deconz.internal.Util;
27 import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage;
28 import org.openhab.binding.deconz.internal.dto.LightMessage;
29 import org.openhab.binding.deconz.internal.dto.LightState;
30 import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient;
31 import org.openhab.binding.deconz.internal.types.ResourceType;
32 import org.openhab.core.library.types.*;
33 import org.openhab.core.thing.ChannelUID;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingStatusDetail;
37 import org.openhab.core.thing.ThingTypeUID;
38 import org.openhab.core.types.Command;
39 import org.openhab.core.types.RefreshType;
40 import org.openhab.core.types.StateDescription;
41 import org.openhab.core.types.StateDescriptionFragmentBuilder;
42 import org.openhab.core.types.UnDefType;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
46 import com.google.gson.Gson;
49 * This light thing doesn't establish any connections, that is done by the bridge Thing.
51 * It waits for the bridge to come online, grab the websocket connection and bridge configuration
52 * and registers to the websocket connection as a listener.
54 * A REST API call is made to get the initial light/rollershutter state.
56 * Every light and rollershutter is supported by this Thing, because a unified state is kept
57 * in {@link #lightStateCache}. Every field that got received by the REST API for this specific
58 * sensor is published to the framework.
60 * @author Jan N. Klug - Initial contribution
63 public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
64 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Set.of(THING_TYPE_COLOR_TEMPERATURE_LIGHT,
65 THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_COLOR_LIGHT, THING_TYPE_EXTENDED_COLOR_LIGHT, THING_TYPE_ONOFF_LIGHT,
66 THING_TYPE_WINDOW_COVERING, THING_TYPE_WARNING_DEVICE);
68 private static final long DEFAULT_COMMAND_EXPIRY_TIME = 250; // in ms
69 private static final int BRIGHTNESS_DIM_STEP = 26; // ~ 10%
71 private final Logger logger = LoggerFactory.getLogger(LightThingHandler.class);
73 private final StateDescriptionProvider stateDescriptionProvider;
75 private long lastCommandExpireTimestamp = 0;
76 private boolean needsPropertyUpdate = false;
79 * The light state. Contains all possible fields for all supported lights
81 private LightState lightStateCache = new LightState();
82 private LightState lastCommand = new LightState();
84 // set defaults, we can override them later if we receive better values
85 private int ctMax = ZCL_CT_MAX;
86 private int ctMin = ZCL_CT_MIN;
88 public LightThingHandler(Thing thing, Gson gson, StateDescriptionProvider stateDescriptionProvider) {
89 super(thing, gson, ResourceType.LIGHTS);
90 this.stateDescriptionProvider = stateDescriptionProvider;
94 public void initialize() {
95 if (thing.getThingTypeUID().equals(THING_TYPE_COLOR_TEMPERATURE_LIGHT)
96 || thing.getThingTypeUID().equals(THING_TYPE_EXTENDED_COLOR_LIGHT)) {
98 Map<String, String> properties = thing.getProperties();
99 String ctMaxString = properties.get(PROPERTY_CT_MAX);
100 ctMax = ctMaxString == null ? ZCL_CT_MAX : Integer.parseInt(ctMaxString);
101 String ctMinString = properties.get(PROPERTY_CT_MIN);
102 ctMin = ctMinString == null ? ZCL_CT_MIN : Integer.parseInt(ctMinString);
104 // minimum and maximum are inverted due to mired/kelvin conversion!
105 StateDescription stateDescription = StateDescriptionFragmentBuilder.create()
106 .withMinimum(new BigDecimal(miredToKelvin(ctMax)))
107 .withMaximum(new BigDecimal(miredToKelvin(ctMin))).build().toStateDescription();
108 if (stateDescription != null) {
109 stateDescriptionProvider.setDescription(new ChannelUID(thing.getUID(), CHANNEL_COLOR_TEMPERATURE),
112 logger.warn("Failed to create state description in thing {}", thing.getUID());
114 } catch (NumberFormatException e) {
115 needsPropertyUpdate = true;
122 public void handleCommand(ChannelUID channelUID, Command command) {
123 if (command instanceof RefreshType) {
124 valueUpdated(channelUID.getId(), lightStateCache);
128 LightState newLightState = new LightState();
129 Boolean currentOn = lightStateCache.on;
130 Integer currentBri = lightStateCache.bri;
132 switch (channelUID.getId()) {
134 if (command instanceof OnOffType) {
135 newLightState.alert = command == OnOffType.ON ? "alert" : "none";
140 if (command instanceof OnOffType) {
141 newLightState.on = (command == OnOffType.ON);
146 case CHANNEL_BRIGHTNESS:
148 if (command instanceof OnOffType) {
149 newLightState.on = (command == OnOffType.ON);
150 } else if (command instanceof IncreaseDecreaseType) {
151 // try to get best value for current brightness
152 int oldBri = currentBri != null ? currentBri
153 : (Boolean.TRUE.equals(currentOn) ? BRIGHTNESS_MAX : BRIGHTNESS_MIN);
154 if (command.equals(IncreaseDecreaseType.INCREASE)) {
155 newLightState.bri = Util.constrainToRange(oldBri + BRIGHTNESS_DIM_STEP, BRIGHTNESS_MIN,
158 newLightState.bri = Util.constrainToRange(oldBri - BRIGHTNESS_DIM_STEP, BRIGHTNESS_MIN,
161 } else if (command instanceof HSBType) {
162 HSBType hsbCommand = (HSBType) command;
164 if ("xy".equals(lightStateCache.colormode)) {
165 PercentType[] xy = hsbCommand.toXY();
167 logger.warn("Failed to convert {} to xy-values", command);
169 newLightState.xy = new double[] { xy[0].doubleValue() / 100.0, xy[1].doubleValue() / 100.0 };
170 newLightState.bri = Util.fromPercentType(hsbCommand.getBrightness());
172 // default is colormode "hs" (used when colormode "hs" is set or colormode is unknown)
173 newLightState.bri = Util.fromPercentType(hsbCommand.getBrightness());
174 newLightState.hue = (int) (hsbCommand.getHue().doubleValue() * HUE_FACTOR);
175 newLightState.sat = Util.fromPercentType(hsbCommand.getSaturation());
177 } else if (command instanceof PercentType) {
178 newLightState.bri = Util.fromPercentType((PercentType) command);
179 } else if (command instanceof DecimalType) {
180 newLightState.bri = ((DecimalType) command).intValue();
185 // send on/off state together with brightness if not already set or unknown
186 Integer newBri = newLightState.bri;
187 if (newBri != null) {
188 newLightState.on = (newBri > 0);
191 // fix sending bri=0 when light is already off
192 if (newBri != null && newBri == 0 && currentOn != null && !currentOn) {
196 Double transitiontime = config.transitiontime;
197 if (transitiontime != null) {
198 // value is in 1/10 seconds
199 newLightState.transitiontime = (int) Math.round(10 * transitiontime);
202 case CHANNEL_COLOR_TEMPERATURE:
203 if (command instanceof DecimalType) {
204 int miredValue = kelvinToMired(((DecimalType) command).intValue());
205 newLightState.ct = constrainToRange(miredValue, ctMin, ctMax);
206 newLightState.on = true;
209 case CHANNEL_POSITION:
210 if (command instanceof UpDownType) {
211 newLightState.on = (command == UpDownType.DOWN);
212 } else if (command == StopMoveType.STOP) {
213 if (currentOn != null && currentOn && currentBri != null && currentBri <= BRIGHTNESS_MAX) {
214 // going down or currently stop (254 because of rounding error)
215 newLightState.on = true;
216 } else if (currentOn != null && !currentOn && currentBri != null && currentBri > BRIGHTNESS_MIN) {
217 // going up or currently stopped
218 newLightState.on = false;
220 } else if (command instanceof PercentType) {
221 newLightState.bri = fromPercentType((PercentType) command);
227 // no supported command
231 Boolean newOn = newLightState.on;
232 if (newOn != null && !newOn) {
233 // if light shall be off, no other commands are allowed, so reset the new light state
234 newLightState.clear();
235 newLightState.on = false;
238 sendCommand(newLightState, command, channelUID, () -> {
239 Integer transitionTime = newLightState.transitiontime;
240 lastCommandExpireTimestamp = System.currentTimeMillis()
241 + (transitionTime != null ? transitionTime : DEFAULT_COMMAND_EXPIRY_TIME);
242 lastCommand = newLightState;
247 protected @Nullable LightMessage parseStateResponse(AsyncHttpClient.Result r) {
248 if (r.getResponseCode() == 403) {
250 } else if (r.getResponseCode() == 200) {
251 LightMessage lightMessage = gson.fromJson(r.getBody(), LightMessage.class);
252 if (needsPropertyUpdate) {
253 // if we did not receive an ctmin/ctmax, then we probably don't need it
254 needsPropertyUpdate = false;
256 Integer ctmax = lightMessage.ctmax;
257 Integer ctmin = lightMessage.ctmin;
258 if (ctmin != null && ctmax != null) {
259 Map<String, String> properties = new HashMap<>(thing.getProperties());
260 properties.put(PROPERTY_CT_MAX,
261 Integer.toString(Util.constrainToRange(ctmax, ZCL_CT_MIN, ZCL_CT_MAX)));
262 properties.put(PROPERTY_CT_MIN,
263 Integer.toString(Util.constrainToRange(ctmin, ZCL_CT_MIN, ZCL_CT_MAX)));
264 updateProperties(properties);
269 throw new IllegalStateException("Unknown status code " + r.getResponseCode() + " for full state request");
274 protected void processStateResponse(@Nullable LightMessage stateResponse) {
275 if (stateResponse == null) {
279 messageReceived(config.id, stateResponse);
282 private void valueUpdated(String channelId, LightState newState) {
283 Integer bri = newState.bri;
284 Integer hue = newState.hue;
285 Integer sat = newState.sat;
286 Boolean on = newState.on;
290 updateState(channelId, "alert".equals(newState.alert) ? OnOffType.ON : OnOffType.OFF);
294 updateState(channelId, OnOffType.from(on));
298 if (on != null && on == false) {
299 updateState(channelId, OnOffType.OFF);
300 } else if (bri != null && "xy".equals(newState.colormode)) {
301 final double @Nullable [] xy = newState.xy;
302 if (xy != null && xy.length == 2) {
303 HSBType color = HSBType.fromXY((float) xy[0], (float) xy[1]);
304 updateState(channelId, new HSBType(color.getHue(), color.getSaturation(), toPercentType(bri)));
306 } else if (bri != null && hue != null && sat != null) {
307 updateState(channelId,
308 new HSBType(new DecimalType(hue / HUE_FACTOR), toPercentType(sat), toPercentType(bri)));
311 case CHANNEL_BRIGHTNESS:
312 if (bri != null && on != null && on) {
313 updateState(channelId, toPercentType(bri));
315 updateState(channelId, OnOffType.OFF);
318 case CHANNEL_COLOR_TEMPERATURE:
319 Integer ct = newState.ct;
320 if (ct != null && ct >= ctMin && ct <= ctMax) {
321 updateState(channelId, new DecimalType(miredToKelvin(ct)));
324 case CHANNEL_POSITION:
326 updateState(channelId, toPercentType(bri));
333 public void messageReceived(String sensorID, DeconzBaseMessage message) {
334 if (message instanceof LightMessage) {
335 LightMessage lightMessage = (LightMessage) message;
336 logger.trace("{} received {}", thing.getUID(), lightMessage);
337 LightState lightState = lightMessage.state;
338 if (lightState != null) {
339 if (lastCommandExpireTimestamp > System.currentTimeMillis()
340 && !lightState.equalsIgnoreNull(lastCommand)) {
341 // skip for SKIP_UPDATE_TIMESPAN after last command if lightState is different from command
342 logger.trace("Ignoring differing update after last command until {}", lastCommandExpireTimestamp);
345 lightStateCache = lightState;
346 if (Boolean.TRUE.equals(lightState.reachable)) {
347 updateStatus(ThingStatus.ONLINE);
348 thing.getChannels().stream().map(c -> c.getUID().getId()).forEach(c -> valueUpdated(c, lightState));
350 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Not reachable");
351 thing.getChannels().stream().map(c -> c.getUID()).forEach(c -> updateState(c, UnDefType.UNDEF));