]> git.basschouten.com Git - openhab-addons.git/blob
26323756de25583554bc67b93c886255afa3a1cd
[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.*;
20 import java.util.stream.Collectors;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.deconz.internal.CommandDescriptionProvider;
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.types.ResourceType;
31 import org.openhab.core.library.types.*;
32 import org.openhab.core.thing.ChannelUID;
33 import org.openhab.core.thing.Thing;
34 import org.openhab.core.thing.ThingStatus;
35 import org.openhab.core.thing.ThingStatusDetail;
36 import org.openhab.core.thing.ThingTypeUID;
37 import org.openhab.core.thing.binding.builder.ChannelBuilder;
38 import org.openhab.core.thing.binding.builder.ThingBuilder;
39 import org.openhab.core.types.*;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 import com.google.gson.Gson;
44
45 /**
46  * This light thing doesn't establish any connections, that is done by the bridge Thing.
47  *
48  * It waits for the bridge to come online, grab the websocket connection and bridge configuration
49  * and registers to the websocket connection as a listener.
50  *
51  * A REST API call is made to get the initial light/rollershutter state.
52  *
53  * Every light and rollershutter is supported by this Thing, because a unified state is kept
54  * in {@link #lightStateCache}. Every field that got received by the REST API for this specific
55  * sensor is published to the framework.
56  *
57  * @author Jan N. Klug - Initial contribution
58  */
59 @NonNullByDefault
60 public class LightThingHandler extends DeconzBaseThingHandler {
61     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPE_UIDS = Set.of(THING_TYPE_COLOR_TEMPERATURE_LIGHT,
62             THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_COLOR_LIGHT, THING_TYPE_EXTENDED_COLOR_LIGHT, THING_TYPE_ONOFF_LIGHT,
63             THING_TYPE_WINDOW_COVERING, THING_TYPE_WARNING_DEVICE, THING_TYPE_DOORLOCK);
64
65     private static final long DEFAULT_COMMAND_EXPIRY_TIME = 250; // in ms
66     private static final int BRIGHTNESS_DIM_STEP = 26; // ~ 10%
67
68     private final Logger logger = LoggerFactory.getLogger(LightThingHandler.class);
69
70     private final StateDescriptionProvider stateDescriptionProvider;
71     private final CommandDescriptionProvider commandDescriptionProvider;
72
73     private long lastCommandExpireTimestamp = 0;
74     private boolean needsPropertyUpdate = false;
75
76     /**
77      * The light state. Contains all possible fields for all supported lights
78      */
79     private LightState lightStateCache = new LightState();
80     private LightState lastCommand = new LightState();
81     private String colorMode = "";
82
83     // set defaults, we can override them later if we receive better values
84     private int ctMax = ZCL_CT_MAX;
85     private int ctMin = ZCL_CT_MIN;
86
87     public LightThingHandler(Thing thing, Gson gson, StateDescriptionProvider stateDescriptionProvider,
88             CommandDescriptionProvider commandDescriptionProvider) {
89         super(thing, gson, ResourceType.LIGHTS);
90         this.stateDescriptionProvider = stateDescriptionProvider;
91         this.commandDescriptionProvider = commandDescriptionProvider;
92     }
93
94     @Override
95     public void initialize() {
96         if (thing.getThingTypeUID().equals(THING_TYPE_COLOR_TEMPERATURE_LIGHT)
97                 || thing.getThingTypeUID().equals(THING_TYPE_EXTENDED_COLOR_LIGHT)) {
98             try {
99                 Map<String, String> properties = thing.getProperties();
100                 String ctMaxString = properties.get(PROPERTY_CT_MAX);
101                 ctMax = ctMaxString == null ? ZCL_CT_MAX : Integer.parseInt(ctMaxString);
102                 String ctMinString = properties.get(PROPERTY_CT_MIN);
103                 ctMin = ctMinString == null ? ZCL_CT_MIN : Integer.parseInt(ctMinString);
104
105                 // minimum and maximum are inverted due to mired/kelvin conversion!
106                 StateDescription stateDescription = StateDescriptionFragmentBuilder.create()
107                         .withMinimum(new BigDecimal(miredToKelvin(ctMax)))
108                         .withMaximum(new BigDecimal(miredToKelvin(ctMin))).build().toStateDescription();
109                 if (stateDescription != null) {
110                     stateDescriptionProvider.setDescription(new ChannelUID(thing.getUID(), CHANNEL_COLOR_TEMPERATURE),
111                             stateDescription);
112                 } else {
113                     logger.warn("Failed to create state description in thing {}", thing.getUID());
114                 }
115             } catch (NumberFormatException e) {
116                 needsPropertyUpdate = true;
117             }
118         }
119         ThingConfig thingConfig = getConfigAs(ThingConfig.class);
120         colorMode = thingConfig.colormode;
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 StringType) {
139                     newLightState.alert = command.toString();
140                 } else {
141                     return;
142                 }
143                 break;
144             case CHANNEL_EFFECT:
145                 if (command instanceof StringType) {
146                     // effect command only allowed for lights that are turned on
147                     newLightState.on = true;
148                     newLightState.effect = command.toString();
149                 } else {
150                     return;
151                 }
152                 break;
153             case CHANNEL_EFFECT_SPEED:
154                 if (command instanceof DecimalType) {
155                     newLightState.on = true;
156                     newLightState.effectSpeed = Util.constrainToRange(((DecimalType) command).intValue(), 0, 10);
157                 } else {
158                     return;
159                 }
160                 break;
161             case CHANNEL_SWITCH:
162             case CHANNEL_LOCK:
163                 if (command instanceof OnOffType) {
164                     newLightState.on = (command == OnOffType.ON);
165                 } else {
166                     return;
167                 }
168                 break;
169             case CHANNEL_BRIGHTNESS:
170             case CHANNEL_COLOR:
171                 if (command instanceof OnOffType) {
172                     newLightState.on = (command == OnOffType.ON);
173                 } else if (command instanceof IncreaseDecreaseType) {
174                     // try to get best value for current brightness
175                     int oldBri = currentBri != null ? currentBri
176                             : (Boolean.TRUE.equals(currentOn) ? BRIGHTNESS_MAX : BRIGHTNESS_MIN);
177                     if (command.equals(IncreaseDecreaseType.INCREASE)) {
178                         newLightState.bri = Util.constrainToRange(oldBri + BRIGHTNESS_DIM_STEP, BRIGHTNESS_MIN,
179                                 BRIGHTNESS_MAX);
180                     } else {
181                         newLightState.bri = Util.constrainToRange(oldBri - BRIGHTNESS_DIM_STEP, BRIGHTNESS_MIN,
182                                 BRIGHTNESS_MAX);
183                     }
184                 } else if (command instanceof HSBType) {
185                     HSBType hsbCommand = (HSBType) command;
186                     if ("xy".equals(colorMode)) {
187                         PercentType[] xy = hsbCommand.toXY();
188                         if (xy.length < 2) {
189                             logger.warn("Failed to convert {} to xy-values", command);
190                         }
191                         newLightState.xy = new double[] { xy[0].doubleValue() / 100.0, xy[1].doubleValue() / 100.0 };
192                         newLightState.bri = Util.fromPercentType(hsbCommand.getBrightness());
193                     } else {
194                         // default is colormode "hs" (used when colormode "hs" is set or colormode is unknown)
195                         newLightState.bri = Util.fromPercentType(hsbCommand.getBrightness());
196                         newLightState.hue = (int) (hsbCommand.getHue().doubleValue() * HUE_FACTOR);
197                         newLightState.sat = Util.fromPercentType(hsbCommand.getSaturation());
198                     }
199                 } else if (command instanceof PercentType) {
200                     newLightState.bri = Util.fromPercentType((PercentType) command);
201                 } else if (command instanceof DecimalType) {
202                     newLightState.bri = ((DecimalType) command).intValue();
203                 } else {
204                     return;
205                 }
206
207                 // send on/off state together with brightness if not already set or unknown
208                 Integer newBri = newLightState.bri;
209                 if (newBri != null) {
210                     newLightState.on = (newBri > 0);
211                 }
212
213                 // fix sending bri=0 when light is already off
214                 if (newBri != null && newBri == 0 && currentOn != null && !currentOn) {
215                     return;
216                 }
217
218                 Double transitiontime = config.transitiontime;
219                 if (transitiontime != null) {
220                     // value is in 1/10 seconds
221                     newLightState.transitiontime = (int) Math.round(10 * transitiontime);
222                 }
223                 break;
224             case CHANNEL_COLOR_TEMPERATURE:
225                 if (command instanceof DecimalType) {
226                     int miredValue = kelvinToMired(((DecimalType) command).intValue());
227                     newLightState.ct = constrainToRange(miredValue, ctMin, ctMax);
228                     newLightState.on = true;
229                 }
230                 break;
231             case CHANNEL_POSITION:
232                 if (command instanceof UpDownType) {
233                     newLightState.on = (command == UpDownType.DOWN);
234                 } else if (command == StopMoveType.STOP) {
235                     if (currentOn != null && currentOn && currentBri != null && currentBri <= BRIGHTNESS_MAX) {
236                         // going down or currently stop (254 because of rounding error)
237                         newLightState.on = true;
238                     } else if (currentOn != null && !currentOn && currentBri != null && currentBri > BRIGHTNESS_MIN) {
239                         // going up or currently stopped
240                         newLightState.on = false;
241                     }
242                 } else if (command instanceof PercentType) {
243                     newLightState.bri = fromPercentType((PercentType) command);
244                 } else {
245                     return;
246                 }
247                 break;
248             default:
249                 // no supported command
250                 return;
251         }
252
253         Boolean newOn = newLightState.on;
254         if (newOn != null && !newOn) {
255             // if light shall be off, no other commands are allowed, so reset the new light state
256             newLightState.clear();
257             newLightState.on = false;
258         }
259
260         sendCommand(newLightState, command, channelUID, () -> {
261             Integer transitionTime = newLightState.transitiontime;
262             lastCommandExpireTimestamp = System.currentTimeMillis()
263                     + (transitionTime != null ? transitionTime : DEFAULT_COMMAND_EXPIRY_TIME);
264             lastCommand = newLightState;
265         });
266     }
267
268     @Override
269     protected void processStateResponse(DeconzBaseMessage stateResponse) {
270         if (!(stateResponse instanceof LightMessage)) {
271             return;
272         }
273
274         LightMessage lightMessage = (LightMessage) stateResponse;
275
276         if (needsPropertyUpdate) {
277             // if we did not receive an ctmin/ctmax, then we probably don't need it
278             needsPropertyUpdate = false;
279
280             Integer ctmax = lightMessage.ctmax;
281             Integer ctmin = lightMessage.ctmin;
282             if (ctmin != null && ctmax != null) {
283                 Map<String, String> properties = new HashMap<>(thing.getProperties());
284                 properties.put(PROPERTY_CT_MAX, Integer.toString(Util.constrainToRange(ctmax, ZCL_CT_MIN, ZCL_CT_MAX)));
285                 properties.put(PROPERTY_CT_MIN, Integer.toString(Util.constrainToRange(ctmin, ZCL_CT_MIN, ZCL_CT_MAX)));
286                 updateProperties(properties);
287             }
288         }
289
290         LightState lightState = lightMessage.state;
291         if (lightState != null && lightState.effect != null) {
292             checkAndUpdateEffectChannels(lightMessage);
293         }
294
295         messageReceived(config.id, lightMessage);
296     }
297
298     private enum EffectLightModel {
299         LIDL_MELINARA,
300         TINT_MUELLER,
301         UNKNOWN;
302     }
303
304     private void checkAndUpdateEffectChannels(LightMessage lightMessage) {
305         EffectLightModel model = EffectLightModel.UNKNOWN;
306         // try to determine which model we have
307         if (lightMessage.manufacturername.equals("_TZE200_s8gkrkxk")) {
308             // the LIDL Melinara string does not report a proper model name
309             model = EffectLightModel.LIDL_MELINARA;
310         } else if (lightMessage.manufacturername.equals("MLI")) {
311             model = EffectLightModel.TINT_MUELLER;
312         } else {
313             logger.info("Could not determine effect light type for thing {}, please request adding support on GitHub.",
314                     thing.getUID());
315         }
316
317         ChannelUID effectChannelUID = new ChannelUID(thing.getUID(), CHANNEL_EFFECT);
318         ChannelUID effectSpeedChannelUID = new ChannelUID(thing.getUID(), CHANNEL_EFFECT_SPEED);
319
320         if (thing.getChannel(CHANNEL_EFFECT) == null) {
321             ThingBuilder thingBuilder = editThing();
322             thingBuilder.withChannel(
323                     ChannelBuilder.create(effectChannelUID, "String").withType(CHANNEL_EFFECT_TYPE_UID).build());
324             if (model == EffectLightModel.LIDL_MELINARA) {
325                 // additional channels
326                 thingBuilder.withChannel(ChannelBuilder.create(effectSpeedChannelUID, "Number")
327                         .withType(CHANNEL_EFFECT_SPEED_TYPE_UID).build());
328             }
329             updateThing(thingBuilder.build());
330         }
331
332         switch (model) {
333             case LIDL_MELINARA:
334                 List<String> options = List.of("none", "steady", "snow", "rainbow", "snake", "tinkle", "fireworks",
335                         "flag", "waves", "updown", "vintage", "fading", "collide", "strobe", "sparkles", "carnival",
336                         "glow");
337                 commandDescriptionProvider.setDescription(effectChannelUID,
338                         CommandDescriptionBuilder.create().withCommandOptions(toCommandOptionList(options)).build());
339                 break;
340             case TINT_MUELLER:
341                 options = List.of("none", "colorloop", "sunset", "party", "worklight", "campfire", "romance",
342                         "nightlight");
343                 commandDescriptionProvider.setDescription(effectChannelUID,
344                         CommandDescriptionBuilder.create().withCommandOptions(toCommandOptionList(options)).build());
345                 break;
346             default:
347                 options = List.of("none", "colorloop");
348                 commandDescriptionProvider.setDescription(effectChannelUID,
349                         CommandDescriptionBuilder.create().withCommandOptions(toCommandOptionList(options)).build());
350
351         }
352     }
353
354     private List<CommandOption> toCommandOptionList(List<String> options) {
355         return options.stream().map(c -> new CommandOption(c, c)).collect(Collectors.toList());
356     }
357
358     private void valueUpdated(String channelId, LightState newState) {
359         Integer bri = newState.bri;
360         Integer hue = newState.hue;
361         Integer sat = newState.sat;
362         Boolean on = newState.on;
363
364         switch (channelId) {
365             case CHANNEL_ALERT:
366                 String alert = newState.alert;
367                 if (alert != null) {
368                     updateState(channelId, new StringType(alert));
369                 }
370                 break;
371             case CHANNEL_SWITCH:
372             case CHANNEL_LOCK:
373                 if (on != null) {
374                     updateState(channelId, OnOffType.from(on));
375                 }
376                 break;
377             case CHANNEL_COLOR:
378                 if (on != null && on == false) {
379                     updateState(channelId, OnOffType.OFF);
380                 } else if (bri != null && "xy".equals(newState.colormode)) {
381                     final double @Nullable [] xy = newState.xy;
382                     if (xy != null && xy.length == 2) {
383                         HSBType color = HSBType.fromXY((float) xy[0], (float) xy[1]);
384                         updateState(channelId, new HSBType(color.getHue(), color.getSaturation(), toPercentType(bri)));
385                     }
386                 } else if (bri != null && hue != null && sat != null) {
387                     updateState(channelId,
388                             new HSBType(new DecimalType(hue / HUE_FACTOR), toPercentType(sat), toPercentType(bri)));
389                 }
390                 break;
391             case CHANNEL_BRIGHTNESS:
392                 if (bri != null && on != null && on) {
393                     updateState(channelId, toPercentType(bri));
394                 } else {
395                     updateState(channelId, OnOffType.OFF);
396                 }
397                 break;
398             case CHANNEL_COLOR_TEMPERATURE:
399                 Integer ct = newState.ct;
400                 if (ct != null && ct >= ctMin && ct <= ctMax) {
401                     updateState(channelId, new DecimalType(miredToKelvin(ct)));
402                 }
403                 break;
404             case CHANNEL_POSITION:
405                 if (bri != null) {
406                     updateState(channelId, toPercentType(bri));
407                 }
408                 break;
409             case CHANNEL_EFFECT:
410                 String effect = newState.effect;
411                 if (effect != null) {
412                     updateState(channelId, new StringType(effect));
413                 }
414                 break;
415             case CHANNEL_EFFECT_SPEED:
416                 Integer effectSpeed = newState.effectSpeed;
417                 if (effectSpeed != null) {
418                     updateState(channelId, new DecimalType(effectSpeed));
419                 }
420                 break;
421             default:
422         }
423     }
424
425     @Override
426     public void messageReceived(String sensorID, DeconzBaseMessage message) {
427         if (message instanceof LightMessage) {
428             LightMessage lightMessage = (LightMessage) message;
429             logger.trace("{} received {}", thing.getUID(), lightMessage);
430             LightState lightState = lightMessage.state;
431             if (lightState != null) {
432                 if (lastCommandExpireTimestamp > System.currentTimeMillis()
433                         && !lightState.equalsIgnoreNull(lastCommand)) {
434                     // skip for SKIP_UPDATE_TIMESPAN after last command if lightState is different from command
435                     logger.trace("Ignoring differing update after last command until {}", lastCommandExpireTimestamp);
436                     return;
437                 }
438                 if (colorMode.isEmpty()) {
439                     String cmode = lightState.colormode;
440                     if (cmode != null && ("hs".equals(cmode) || "xy".equals(cmode))) {
441                         // only set the color mode if it is hs or xy, not ct
442                         colorMode = cmode;
443                     }
444                 }
445                 lightStateCache = lightState;
446                 if (Boolean.TRUE.equals(lightState.reachable)) {
447                     updateStatus(ThingStatus.ONLINE);
448                     thing.getChannels().stream().map(c -> c.getUID().getId()).forEach(c -> valueUpdated(c, lightState));
449                 } else {
450                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Not reachable");
451                     thing.getChannels().stream().map(c -> c.getUID()).forEach(c -> updateState(c, UnDefType.UNDEF));
452                 }
453             }
454         }
455     }
456 }