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