]> git.basschouten.com Git - openhab-addons.git/blob
269a98ccaa42e49b7de79e72193ef15c097c3a98
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.deconz.internal.handler;
14
15 import static org.openhab.binding.deconz.internal.BindingConstants.*;
16 import static org.openhab.binding.deconz.internal.Util.*;
17
18 import java.math.BigDecimal;
19 import java.util.HashMap;
20 import java.util.Map;
21 import java.util.Set;
22
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;
45
46 import com.google.gson.Gson;
47
48 /**
49  * This light thing doesn't establish any connections, that is done by the bridge Thing.
50  *
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.
53  *
54  * A REST API call is made to get the initial light/rollershutter state.
55  *
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.
59  *
60  * @author Jan N. Klug - Initial contribution
61  */
62 @NonNullByDefault
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, THING_TYPE_DOORLOCK);
67
68     private static final long DEFAULT_COMMAND_EXPIRY_TIME = 250; // in ms
69     private static final int BRIGHTNESS_DIM_STEP = 26; // ~ 10%
70
71     private final Logger logger = LoggerFactory.getLogger(LightThingHandler.class);
72
73     private final StateDescriptionProvider stateDescriptionProvider;
74
75     private long lastCommandExpireTimestamp = 0;
76     private boolean needsPropertyUpdate = false;
77
78     /**
79      * The light state. Contains all possible fields for all supported lights
80      */
81     private LightState lightStateCache = new LightState();
82     private LightState lastCommand = new LightState();
83
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;
87
88     public LightThingHandler(Thing thing, Gson gson, StateDescriptionProvider stateDescriptionProvider) {
89         super(thing, gson, ResourceType.LIGHTS);
90         this.stateDescriptionProvider = stateDescriptionProvider;
91     }
92
93     @Override
94     public void initialize() {
95         if (thing.getThingTypeUID().equals(THING_TYPE_COLOR_TEMPERATURE_LIGHT)
96                 || thing.getThingTypeUID().equals(THING_TYPE_EXTENDED_COLOR_LIGHT)) {
97             try {
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);
103
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),
110                             stateDescription);
111                 } else {
112                     logger.warn("Failed to create state description in thing {}", thing.getUID());
113                 }
114             } catch (NumberFormatException e) {
115                 needsPropertyUpdate = true;
116             }
117         }
118         super.initialize();
119     }
120
121     @Override
122     public void handleCommand(ChannelUID channelUID, Command command) {
123         if (command instanceof RefreshType) {
124             valueUpdated(channelUID.getId(), lightStateCache);
125             return;
126         }
127
128         LightState newLightState = new LightState();
129         Boolean currentOn = lightStateCache.on;
130         Integer currentBri = lightStateCache.bri;
131
132         switch (channelUID.getId()) {
133             case CHANNEL_ALERT:
134                 if (command instanceof OnOffType) {
135                     newLightState.alert = command == OnOffType.ON ? "alert" : "none";
136                 } else {
137                     return;
138                 }
139             case CHANNEL_SWITCH:
140             case CHANNEL_LOCK:
141                 if (command instanceof OnOffType) {
142                     newLightState.on = (command == OnOffType.ON);
143                 } else {
144                     return;
145                 }
146                 break;
147             case CHANNEL_BRIGHTNESS:
148             case CHANNEL_COLOR:
149                 if (command instanceof OnOffType) {
150                     newLightState.on = (command == OnOffType.ON);
151                 } else if (command instanceof IncreaseDecreaseType) {
152                     // try to get best value for current brightness
153                     int oldBri = currentBri != null ? currentBri
154                             : (Boolean.TRUE.equals(currentOn) ? BRIGHTNESS_MAX : BRIGHTNESS_MIN);
155                     if (command.equals(IncreaseDecreaseType.INCREASE)) {
156                         newLightState.bri = Util.constrainToRange(oldBri + BRIGHTNESS_DIM_STEP, BRIGHTNESS_MIN,
157                                 BRIGHTNESS_MAX);
158                     } else {
159                         newLightState.bri = Util.constrainToRange(oldBri - BRIGHTNESS_DIM_STEP, BRIGHTNESS_MIN,
160                                 BRIGHTNESS_MAX);
161                     }
162                 } else if (command instanceof HSBType) {
163                     HSBType hsbCommand = (HSBType) command;
164
165                     if ("xy".equals(lightStateCache.colormode)) {
166                         PercentType[] xy = hsbCommand.toXY();
167                         if (xy.length < 2) {
168                             logger.warn("Failed to convert {} to xy-values", command);
169                         }
170                         newLightState.xy = new double[] { xy[0].doubleValue() / 100.0, xy[1].doubleValue() / 100.0 };
171                         newLightState.bri = Util.fromPercentType(hsbCommand.getBrightness());
172                     } else {
173                         // default is colormode "hs" (used when colormode "hs" is set or colormode is unknown)
174                         newLightState.bri = Util.fromPercentType(hsbCommand.getBrightness());
175                         newLightState.hue = (int) (hsbCommand.getHue().doubleValue() * HUE_FACTOR);
176                         newLightState.sat = Util.fromPercentType(hsbCommand.getSaturation());
177                     }
178                 } else if (command instanceof PercentType) {
179                     newLightState.bri = Util.fromPercentType((PercentType) command);
180                 } else if (command instanceof DecimalType) {
181                     newLightState.bri = ((DecimalType) command).intValue();
182                 } else {
183                     return;
184                 }
185
186                 // send on/off state together with brightness if not already set or unknown
187                 Integer newBri = newLightState.bri;
188                 if (newBri != null) {
189                     newLightState.on = (newBri > 0);
190                 }
191
192                 // fix sending bri=0 when light is already off
193                 if (newBri != null && newBri == 0 && currentOn != null && !currentOn) {
194                     return;
195                 }
196
197                 Double transitiontime = config.transitiontime;
198                 if (transitiontime != null) {
199                     // value is in 1/10 seconds
200                     newLightState.transitiontime = (int) Math.round(10 * transitiontime);
201                 }
202                 break;
203             case CHANNEL_COLOR_TEMPERATURE:
204                 if (command instanceof DecimalType) {
205                     int miredValue = kelvinToMired(((DecimalType) command).intValue());
206                     newLightState.ct = constrainToRange(miredValue, ctMin, ctMax);
207                     newLightState.on = true;
208                 }
209                 break;
210             case CHANNEL_POSITION:
211                 if (command instanceof UpDownType) {
212                     newLightState.on = (command == UpDownType.DOWN);
213                 } else if (command == StopMoveType.STOP) {
214                     if (currentOn != null && currentOn && currentBri != null && currentBri <= BRIGHTNESS_MAX) {
215                         // going down or currently stop (254 because of rounding error)
216                         newLightState.on = true;
217                     } else if (currentOn != null && !currentOn && currentBri != null && currentBri > BRIGHTNESS_MIN) {
218                         // going up or currently stopped
219                         newLightState.on = false;
220                     }
221                 } else if (command instanceof PercentType) {
222                     newLightState.bri = fromPercentType((PercentType) command);
223                 } else {
224                     return;
225                 }
226                 break;
227             default:
228                 // no supported command
229                 return;
230         }
231
232         Boolean newOn = newLightState.on;
233         if (newOn != null && !newOn) {
234             // if light shall be off, no other commands are allowed, so reset the new light state
235             newLightState.clear();
236             newLightState.on = false;
237         }
238
239         sendCommand(newLightState, command, channelUID, () -> {
240             Integer transitionTime = newLightState.transitiontime;
241             lastCommandExpireTimestamp = System.currentTimeMillis()
242                     + (transitionTime != null ? transitionTime : DEFAULT_COMMAND_EXPIRY_TIME);
243             lastCommand = newLightState;
244         });
245     }
246
247     @Override
248     protected @Nullable LightMessage parseStateResponse(AsyncHttpClient.Result r) {
249         if (r.getResponseCode() == 403) {
250             return null;
251         } else if (r.getResponseCode() == 200) {
252             LightMessage lightMessage = gson.fromJson(r.getBody(), LightMessage.class);
253             if (needsPropertyUpdate) {
254                 // if we did not receive an ctmin/ctmax, then we probably don't need it
255                 needsPropertyUpdate = false;
256
257                 Integer ctmax = lightMessage.ctmax;
258                 Integer ctmin = lightMessage.ctmin;
259                 if (ctmin != null && ctmax != null) {
260                     Map<String, String> properties = new HashMap<>(thing.getProperties());
261                     properties.put(PROPERTY_CT_MAX,
262                             Integer.toString(Util.constrainToRange(ctmax, ZCL_CT_MIN, ZCL_CT_MAX)));
263                     properties.put(PROPERTY_CT_MIN,
264                             Integer.toString(Util.constrainToRange(ctmin, ZCL_CT_MIN, ZCL_CT_MAX)));
265                     updateProperties(properties);
266                 }
267             }
268             return lightMessage;
269         } else {
270             throw new IllegalStateException("Unknown status code " + r.getResponseCode() + " for full state request");
271         }
272     }
273
274     @Override
275     protected void processStateResponse(@Nullable LightMessage stateResponse) {
276         if (stateResponse == null) {
277             return;
278         }
279
280         messageReceived(config.id, stateResponse);
281     }
282
283     private void valueUpdated(String channelId, LightState newState) {
284         Integer bri = newState.bri;
285         Integer hue = newState.hue;
286         Integer sat = newState.sat;
287         Boolean on = newState.on;
288
289         switch (channelId) {
290             case CHANNEL_ALERT:
291                 updateState(channelId, "alert".equals(newState.alert) ? OnOffType.ON : OnOffType.OFF);
292                 break;
293             case CHANNEL_SWITCH:
294             case CHANNEL_LOCK:
295                 if (on != null) {
296                     updateState(channelId, OnOffType.from(on));
297                 }
298                 break;
299             case CHANNEL_COLOR:
300                 if (on != null && on == false) {
301                     updateState(channelId, OnOffType.OFF);
302                 } else if (bri != null && "xy".equals(newState.colormode)) {
303                     final double @Nullable [] xy = newState.xy;
304                     if (xy != null && xy.length == 2) {
305                         HSBType color = HSBType.fromXY((float) xy[0], (float) xy[1]);
306                         updateState(channelId, new HSBType(color.getHue(), color.getSaturation(), toPercentType(bri)));
307                     }
308                 } else if (bri != null && hue != null && sat != null) {
309                     updateState(channelId,
310                             new HSBType(new DecimalType(hue / HUE_FACTOR), toPercentType(sat), toPercentType(bri)));
311                 }
312                 break;
313             case CHANNEL_BRIGHTNESS:
314                 if (bri != null && on != null && on) {
315                     updateState(channelId, toPercentType(bri));
316                 } else {
317                     updateState(channelId, OnOffType.OFF);
318                 }
319                 break;
320             case CHANNEL_COLOR_TEMPERATURE:
321                 Integer ct = newState.ct;
322                 if (ct != null && ct >= ctMin && ct <= ctMax) {
323                     updateState(channelId, new DecimalType(miredToKelvin(ct)));
324                 }
325                 break;
326             case CHANNEL_POSITION:
327                 if (bri != null) {
328                     updateState(channelId, toPercentType(bri));
329                 }
330             default:
331         }
332     }
333
334     @Override
335     public void messageReceived(String sensorID, DeconzBaseMessage message) {
336         if (message instanceof LightMessage) {
337             LightMessage lightMessage = (LightMessage) message;
338             logger.trace("{} received {}", thing.getUID(), lightMessage);
339             LightState lightState = lightMessage.state;
340             if (lightState != null) {
341                 if (lastCommandExpireTimestamp > System.currentTimeMillis()
342                         && !lightState.equalsIgnoreNull(lastCommand)) {
343                     // skip for SKIP_UPDATE_TIMESPAN after last command if lightState is different from command
344                     logger.trace("Ignoring differing update after last command until {}", lastCommandExpireTimestamp);
345                     return;
346                 }
347                 lightStateCache = lightState;
348                 if (Boolean.TRUE.equals(lightState.reachable)) {
349                     updateStatus(ThingStatus.ONLINE);
350                     thing.getChannels().stream().map(c -> c.getUID().getId()).forEach(c -> valueUpdated(c, lightState));
351                 } else {
352                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Not reachable");
353                     thing.getChannels().stream().map(c -> c.getUID()).forEach(c -> updateState(c, UnDefType.UNDEF));
354                 }
355             }
356         }
357     }
358 }