]> git.basschouten.com Git - openhab-addons.git/blob
8a1a361d55e70d331e25e2d6ff9ff7de0fce7910
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.lifx.internal.handler;
14
15 import static org.openhab.binding.lifx.internal.LifxBindingConstants.*;
16 import static org.openhab.binding.lifx.internal.LifxProduct.Feature.*;
17 import static org.openhab.binding.lifx.internal.util.LifxMessageUtil.*;
18
19 import java.net.InetSocketAddress;
20 import java.time.Duration;
21 import java.time.LocalDateTime;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.HashMap;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Objects;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.locks.ReentrantLock;
30
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.lifx.internal.LifxBindingConstants;
34 import org.openhab.binding.lifx.internal.LifxChannelFactory;
35 import org.openhab.binding.lifx.internal.LifxLightCommunicationHandler;
36 import org.openhab.binding.lifx.internal.LifxLightConfig;
37 import org.openhab.binding.lifx.internal.LifxLightContext;
38 import org.openhab.binding.lifx.internal.LifxLightCurrentStateUpdater;
39 import org.openhab.binding.lifx.internal.LifxLightOnlineStateUpdater;
40 import org.openhab.binding.lifx.internal.LifxLightPropertiesUpdater;
41 import org.openhab.binding.lifx.internal.LifxLightState;
42 import org.openhab.binding.lifx.internal.LifxLightStateChanger;
43 import org.openhab.binding.lifx.internal.LifxProduct;
44 import org.openhab.binding.lifx.internal.LifxProduct.Features;
45 import org.openhab.binding.lifx.internal.dto.Effect;
46 import org.openhab.binding.lifx.internal.dto.GetHevCycleRequest;
47 import org.openhab.binding.lifx.internal.dto.GetLightInfraredRequest;
48 import org.openhab.binding.lifx.internal.dto.GetLightPowerRequest;
49 import org.openhab.binding.lifx.internal.dto.GetRequest;
50 import org.openhab.binding.lifx.internal.dto.GetTileEffectRequest;
51 import org.openhab.binding.lifx.internal.dto.GetWifiInfoRequest;
52 import org.openhab.binding.lifx.internal.dto.HevCycleState;
53 import org.openhab.binding.lifx.internal.dto.Packet;
54 import org.openhab.binding.lifx.internal.dto.PowerState;
55 import org.openhab.binding.lifx.internal.dto.SignalStrength;
56 import org.openhab.binding.lifx.internal.fields.HSBK;
57 import org.openhab.binding.lifx.internal.fields.MACAddress;
58 import org.openhab.core.config.core.Configuration;
59 import org.openhab.core.library.types.DecimalType;
60 import org.openhab.core.library.types.HSBType;
61 import org.openhab.core.library.types.IncreaseDecreaseType;
62 import org.openhab.core.library.types.OnOffType;
63 import org.openhab.core.library.types.PercentType;
64 import org.openhab.core.library.types.QuantityType;
65 import org.openhab.core.library.types.StringType;
66 import org.openhab.core.library.unit.Units;
67 import org.openhab.core.thing.Channel;
68 import org.openhab.core.thing.ChannelUID;
69 import org.openhab.core.thing.Thing;
70 import org.openhab.core.thing.ThingStatus;
71 import org.openhab.core.thing.ThingStatusDetail;
72 import org.openhab.core.thing.ThingStatusInfo;
73 import org.openhab.core.thing.binding.BaseThingHandler;
74 import org.openhab.core.types.Command;
75 import org.openhab.core.types.RefreshType;
76 import org.openhab.core.types.State;
77 import org.slf4j.Logger;
78 import org.slf4j.LoggerFactory;
79
80 /**
81  * The {@link LifxLightHandler} is responsible for handling commands, which are
82  * sent to one of the light channels.
83  *
84  * @author Dennis Nobel - Initial contribution
85  * @author Stefan Bußweiler - Added new thing status handling
86  * @author Karel Goderis - Rewrite for Firmware V2, and remove dependency on external libraries
87  * @author Kai Kreuzer - Added configurable transition time and small fixes
88  * @author Wouter Born - Decomposed class into separate objects
89  * @author Pauli Anttila - Added power on temperature and color features.
90  */
91 @NonNullByDefault
92 public class LifxLightHandler extends BaseThingHandler {
93
94     private final Logger logger = LoggerFactory.getLogger(LifxLightHandler.class);
95
96     private static final Duration MIN_STATUS_INFO_UPDATE_INTERVAL = Duration.ofSeconds(1);
97     private static final Duration MAX_STATE_CHANGE_DURATION = Duration.ofSeconds(4);
98
99     private final LifxChannelFactory channelFactory;
100     private @NonNullByDefault({}) Features features;
101
102     private Duration hevCycleDuration = Duration.ZERO;
103     private @Nullable PercentType powerOnBrightness;
104     private @Nullable HSBType powerOnColor;
105     private @Nullable PercentType powerOnTemperature;
106     private Double effectMorphSpeed = 3.0;
107     private Double effectFlameSpeed = 4.0;
108
109     private @NonNullByDefault({}) String logId;
110
111     private final ReentrantLock lock = new ReentrantLock();
112
113     private @NonNullByDefault({}) CurrentLightState currentLightState;
114     private @NonNullByDefault({}) LifxLightState pendingLightState;
115
116     private Map<String, @Nullable State> channelStates = new HashMap<>();
117     private @Nullable ThingStatusInfo statusInfo;
118     private LocalDateTime lastStatusInfoUpdate = LocalDateTime.MIN;
119
120     private @NonNullByDefault({}) LifxLightCommunicationHandler communicationHandler;
121     private @NonNullByDefault({}) LifxLightCurrentStateUpdater currentStateUpdater;
122     private @NonNullByDefault({}) LifxLightStateChanger lightStateChanger;
123     private @NonNullByDefault({}) LifxLightOnlineStateUpdater onlineStateUpdater;
124     private @NonNullByDefault({}) LifxLightPropertiesUpdater propertiesUpdater;
125
126     public class CurrentLightState extends LifxLightState {
127
128         public boolean isOnline() {
129             return thing.getStatus() == ThingStatus.ONLINE;
130         }
131
132         public boolean isOffline() {
133             return thing.getStatus() == ThingStatus.OFFLINE;
134         }
135
136         public void setOnline() {
137             updateStatusIfChanged(ThingStatus.ONLINE);
138         }
139
140         public void setOnline(MACAddress macAddress) {
141             updateStatusIfChanged(ThingStatus.ONLINE);
142             Configuration configuration = editConfiguration();
143             configuration.put(LifxBindingConstants.CONFIG_PROPERTY_DEVICE_ID, macAddress.getAsLabel());
144             updateConfiguration(configuration);
145         }
146
147         public void setOffline() {
148             updateStatusIfChanged(ThingStatus.OFFLINE);
149         }
150
151         public void setOfflineByCommunicationError() {
152             updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
153         }
154
155         @Override
156         public void setColors(HSBK[] colors) {
157             if (!isStateChangePending() || isPendingColorStateChangesApplied(getPowerState(), colors)) {
158                 PowerState powerState = isStateChangePending() ? pendingLightState.getPowerState() : getPowerState();
159                 updateColorChannels(powerState, colors);
160             }
161             super.setColors(colors);
162         }
163
164         @Override
165         public void setPowerState(PowerState powerState) {
166             if (!isStateChangePending() || isPendingColorStateChangesApplied(powerState, getColors())) {
167                 HSBK[] colors = isStateChangePending() ? pendingLightState.getColors() : getColors();
168                 updateColorChannels(powerState, colors);
169             }
170             super.setPowerState(powerState);
171         }
172
173         private boolean isPendingColorStateChangesApplied(@Nullable PowerState powerState, HSBK[] colors) {
174             return powerState != null && powerState.equals(pendingLightState.getPowerState())
175                     && Arrays.equals(colors, pendingLightState.getColors());
176         }
177
178         private void updateColorChannels(@Nullable PowerState powerState, HSBK[] colors) {
179             HSBK color = colors.length > 0 ? colors[0] : null;
180             HSBK updateColor = nullSafeUpdateColor(powerState, color);
181             HSBType hsb = updateColor.getHSB();
182
183             updateStateIfChanged(CHANNEL_COLOR, hsb);
184             updateStateIfChanged(CHANNEL_BRIGHTNESS, hsb.getBrightness());
185             updateStateIfChanged(CHANNEL_TEMPERATURE,
186                     kelvinToPercentType(updateColor.getKelvin(), features.getTemperatureRange()));
187             updateStateIfChanged(CHANNEL_ABS_TEMPERATURE, new QuantityType(updateColor.getKelvin(), Units.KELVIN));
188
189             updateZoneChannels(powerState, colors);
190         }
191
192         private HSBK nullSafeUpdateColor(@Nullable PowerState powerState, @Nullable HSBK color) {
193             HSBK updateColor = color != null ? color : DEFAULT_COLOR;
194             if (powerState == PowerState.OFF) {
195                 updateColor = new HSBK(updateColor);
196                 updateColor.setBrightness(PercentType.ZERO);
197             }
198             return updateColor;
199         }
200
201         @Override
202         public void setHevCycleState(HevCycleState hevCycleState) {
203             if (!isStateChangePending() || hevCycleState.equals(pendingLightState.getHevCycleState())) {
204                 updateStateIfChanged(CHANNEL_HEV_CYCLE, OnOffType.from(hevCycleState.isEnable()));
205             }
206             super.setHevCycleState(hevCycleState);
207         }
208
209         @Override
210         public void setInfrared(PercentType infrared) {
211             if (!isStateChangePending() || infrared.equals(pendingLightState.getInfrared())) {
212                 updateStateIfChanged(CHANNEL_INFRARED, infrared);
213             }
214             super.setInfrared(infrared);
215         }
216
217         @Override
218         public void setSignalStrength(SignalStrength signalStrength) {
219             updateStateIfChanged(CHANNEL_SIGNAL_STRENGTH, new DecimalType(signalStrength.toQualityRating()));
220             super.setSignalStrength(signalStrength);
221         }
222
223         @Override
224         public void setTileEffect(Effect effect) {
225             updateStateIfChanged(CHANNEL_EFFECT, new StringType(effect.getType().stringValue()));
226             super.setTileEffect(effect);
227         }
228
229         private void updateZoneChannels(@Nullable PowerState powerState, HSBK[] colors) {
230             if (!features.hasFeature(MULTIZONE) || colors.length == 0) {
231                 return;
232             }
233
234             int oldZones = getColors().length;
235             int newZones = colors.length;
236             if (oldZones != newZones) {
237                 addRemoveZoneChannels(newZones);
238             }
239
240             for (int i = 0; i < colors.length; i++) {
241                 HSBK color = colors[i];
242                 HSBK updateColor = nullSafeUpdateColor(powerState, color);
243                 updateStateIfChanged(CHANNEL_COLOR_ZONE + i, updateColor.getHSB());
244                 updateStateIfChanged(CHANNEL_TEMPERATURE_ZONE + i,
245                         kelvinToPercentType(updateColor.getKelvin(), features.getTemperatureRange()));
246                 updateStateIfChanged(CHANNEL_ABS_TEMPERATURE_ZONE + i,
247                         new QuantityType(updateColor.getKelvin(), Units.KELVIN));
248             }
249         }
250     }
251
252     public LifxLightHandler(Thing thing, LifxChannelFactory channelFactory) {
253         super(thing);
254         this.channelFactory = channelFactory;
255     }
256
257     @Override
258     public void initialize() {
259         try {
260             lock.lock();
261
262             LifxLightConfig configuration = getConfigAs(LifxLightConfig.class);
263
264             logId = getLogId(configuration.getMACAddress(), configuration.getHost());
265
266             if (logger.isDebugEnabled()) {
267                 logger.debug("{} : Initializing handler for product {}", logId, getProduct().getName());
268             }
269
270             features = getFeatures();
271
272             powerOnBrightness = getPowerOnBrightness();
273             powerOnColor = getPowerOnColor();
274             powerOnTemperature = getPowerOnTemperature();
275             Double speed = getEffectSpeed(LifxBindingConstants.CONFIG_PROPERTY_EFFECT_MORPH_SPEED);
276             if (speed != null) {
277                 effectMorphSpeed = speed;
278             }
279             speed = getEffectSpeed(LifxBindingConstants.CONFIG_PROPERTY_EFFECT_FLAME_SPEED);
280             if (speed != null) {
281                 effectFlameSpeed = speed;
282             }
283             hevCycleDuration = getHevCycleDuration();
284
285             channelStates.clear();
286             currentLightState = new CurrentLightState();
287             pendingLightState = new LifxLightState();
288
289             LifxLightContext context = new LifxLightContext(logId, features, configuration, currentLightState,
290                     pendingLightState, scheduler);
291
292             communicationHandler = new LifxLightCommunicationHandler(context);
293             currentStateUpdater = new LifxLightCurrentStateUpdater(context, communicationHandler);
294             onlineStateUpdater = new LifxLightOnlineStateUpdater(context, communicationHandler);
295             propertiesUpdater = new LifxLightPropertiesUpdater(context, communicationHandler);
296             propertiesUpdater.addPropertiesUpdateListener(this::updateProperties);
297             lightStateChanger = new LifxLightStateChanger(context, communicationHandler);
298
299             if (configuration.getMACAddress() != null || configuration.getHost() != null) {
300                 communicationHandler.start();
301                 currentStateUpdater.start();
302                 onlineStateUpdater.start();
303                 propertiesUpdater.start();
304                 lightStateChanger.start();
305                 startOrStopSignalStrengthUpdates();
306             } else {
307                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
308                         "Configure a Device ID or Host");
309             }
310         } catch (Exception e) {
311             logger.debug("{} : Error occurred while initializing handler: {}", logId, e.getMessage(), e);
312             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
313         } finally {
314             lock.unlock();
315         }
316     }
317
318     @Override
319     public void dispose() {
320         try {
321             lock.lock();
322
323             logger.debug("{} : Disposing handler", logId);
324
325             if (communicationHandler != null) {
326                 communicationHandler.stop();
327                 communicationHandler = null;
328             }
329
330             if (currentStateUpdater != null) {
331                 currentStateUpdater.stop();
332                 currentStateUpdater = null;
333             }
334
335             if (onlineStateUpdater != null) {
336                 onlineStateUpdater.stop();
337                 onlineStateUpdater = null;
338             }
339
340             if (propertiesUpdater != null) {
341                 propertiesUpdater.stop();
342                 propertiesUpdater.removePropertiesUpdateListener(this::updateProperties);
343                 propertiesUpdater = null;
344             }
345
346             if (lightStateChanger != null) {
347                 lightStateChanger.stop();
348                 lightStateChanger = null;
349             }
350
351             currentLightState = null;
352             pendingLightState = null;
353         } finally {
354             lock.unlock();
355         }
356     }
357
358     public String getLogId(@Nullable MACAddress macAddress, @Nullable InetSocketAddress host) {
359         return (macAddress != null ? macAddress.getHex() : (host != null ? host.getHostString() : "Unknown"));
360     }
361
362     private @Nullable PercentType getPowerOnBrightness() {
363         Channel channel = null;
364
365         if (features.hasFeature(COLOR)) {
366             ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_COLOR);
367             channel = getThing().getChannel(channelUID.getId());
368         } else {
369             ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_BRIGHTNESS);
370             channel = getThing().getChannel(channelUID.getId());
371         }
372
373         if (channel == null) {
374             return null;
375         }
376
377         Configuration configuration = channel.getConfiguration();
378         Object powerOnBrightness = configuration.get(LifxBindingConstants.CONFIG_PROPERTY_POWER_ON_BRIGHTNESS);
379         return powerOnBrightness == null ? null : new PercentType(powerOnBrightness.toString());
380     }
381
382     private @Nullable HSBType getPowerOnColor() {
383         Channel channel = null;
384
385         if (features.hasFeature(COLOR)) {
386             ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_COLOR);
387             channel = getThing().getChannel(channelUID.getId());
388         }
389
390         if (channel == null) {
391             return null;
392         }
393
394         Configuration configuration = channel.getConfiguration();
395         Object powerOnColor = configuration.get(LifxBindingConstants.CONFIG_PROPERTY_POWER_ON_COLOR);
396         return powerOnColor == null ? null : new HSBType(powerOnColor.toString());
397     }
398
399     private @Nullable PercentType getPowerOnTemperature() {
400         ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_TEMPERATURE);
401         Channel channel = getThing().getChannel(channelUID.getId());
402
403         if (channel == null) {
404             return null;
405         }
406
407         Configuration configuration = channel.getConfiguration();
408         Object powerOnTemperature = configuration.get(LifxBindingConstants.CONFIG_PROPERTY_POWER_ON_TEMPERATURE);
409         if (powerOnTemperature != null) {
410             return new PercentType(powerOnTemperature.toString());
411         }
412         return null;
413     }
414
415     private @Nullable Double getEffectSpeed(String parameter) {
416         Channel channel = null;
417
418         if (features.hasFeature(TILE_EFFECT)) {
419             ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_EFFECT);
420             channel = getThing().getChannel(channelUID.getId());
421         }
422
423         if (channel == null) {
424             return null;
425         }
426
427         Configuration configuration = channel.getConfiguration();
428         Object speed = configuration.get(parameter);
429         return speed == null ? null : Double.valueOf(speed.toString());
430     }
431
432     private Duration getHevCycleDuration() {
433         ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_HEV_CYCLE);
434         Channel channel = getThing().getChannel(channelUID.getId());
435
436         if (channel == null) {
437             return Duration.ZERO;
438         }
439
440         Configuration configuration = channel.getConfiguration();
441         Object duration = configuration.get(LifxBindingConstants.CONFIG_PROPERTY_HEV_CYCLE_DURATION);
442         return duration == null ? Duration.ZERO : Duration.ofSeconds(Integer.valueOf(duration.toString()));
443     }
444
445     private Features getFeatures() {
446         LifxProduct product = getProduct();
447
448         String propertyValue = getThing().getProperties().get(LifxBindingConstants.PROPERTY_HOST_VERSION);
449         if (propertyValue == null) {
450             logger.debug("{} : Using features of initial firmware version", logId);
451             return product.getFeatures();
452         }
453
454         logger.debug("{} : Using features of firmware version {}", logId, propertyValue);
455         return product.getFeatures(propertyValue);
456     }
457
458     private LifxProduct getProduct() {
459         String propertyValue = getThing().getProperties().get(LifxBindingConstants.PROPERTY_PRODUCT_ID);
460         if (propertyValue == null) {
461             return LifxProduct.getLikelyProduct(getThing().getThingTypeUID());
462         }
463         try {
464             // Without first conversion to double, on a very first thing creation from discovery inbox,
465             // the product type is incorrectly parsed, as framework passed it as a floating point number
466             // (e.g. 50.0 instead of 50)
467             Double d = Double.valueOf(propertyValue);
468             long productID = d.longValue();
469             return LifxProduct.getProductFromProductID(productID);
470         } catch (IllegalArgumentException e) {
471             return LifxProduct.getLikelyProduct(getThing().getThingTypeUID());
472         }
473     }
474
475     private void addRemoveZoneChannels(int zones) {
476         List<Channel> newChannels = new ArrayList<>();
477
478         // retain non-zone channels
479         for (Channel channel : getThing().getChannels()) {
480             String channelId = channel.getUID().getId();
481             if (!channelId.startsWith(CHANNEL_ABS_TEMPERATURE_ZONE) && !channelId.startsWith(CHANNEL_COLOR_ZONE)
482                     && !channelId.startsWith(CHANNEL_TEMPERATURE_ZONE)) {
483                 newChannels.add(channel);
484             }
485         }
486
487         // add zone channels
488         for (int i = 0; i < zones; i++) {
489             newChannels.add(channelFactory.createColorZoneChannel(getThing().getUID(), i));
490             newChannels.add(channelFactory.createTemperatureZoneChannel(getThing().getUID(), i));
491             newChannels.add(channelFactory.createAbsTemperatureZoneChannel(getThing().getUID(), i));
492         }
493
494         updateThing(editThing().withChannels(newChannels).build());
495
496         Map<String, String> properties = editProperties();
497         properties.put(LifxBindingConstants.PROPERTY_ZONES, Integer.toString(zones));
498         updateProperties(properties);
499     }
500
501     @Override
502     public void channelLinked(ChannelUID channelUID) {
503         super.channelLinked(channelUID);
504         startOrStopSignalStrengthUpdates();
505     }
506
507     @Override
508     public void channelUnlinked(ChannelUID channelUID) {
509         startOrStopSignalStrengthUpdates();
510     }
511
512     private void startOrStopSignalStrengthUpdates() {
513         currentStateUpdater.setUpdateSignalStrength(isLinked(CHANNEL_SIGNAL_STRENGTH));
514     }
515
516     private void sendPacket(Packet packet) {
517         communicationHandler.sendPacket(packet);
518     }
519
520     @Override
521     public void handleCommand(ChannelUID channelUID, Command command) {
522         if (command instanceof RefreshType) {
523             handleRefreshCommand(channelUID);
524         } else {
525             Runnable channelCommandRunnable = getChannelCommandRunnable(channelUID, command);
526             if (channelCommandRunnable == null) {
527                 return;
528             }
529
530             String channelId = channelUID.getId();
531             boolean isHevCycleChannelCommand = CHANNEL_HEV_CYCLE.equals(channelId);
532             boolean isInfraredChannelCommand = CHANNEL_INFRARED.equals(channelId);
533             boolean waitForHevCycleDisabled = false;
534
535             if (getFeatures().hasFeature(HEV) && !isHevCycleChannelCommand) {
536                 LifxLightState lightState = getLightStateForCommand();
537                 HevCycleState currentHevCycleState = lightState.getHevCycleState();
538                 if (currentHevCycleState == null || currentHevCycleState.isEnable()) {
539                     lightState.setHevCycleState(HevCycleState.OFF);
540                     lightState.setPowerState(PowerState.OFF);
541                     waitForHevCycleDisabled = true;
542                 }
543             }
544
545             Runnable compositeCommandsRunnable = () -> {
546                 channelCommandRunnable.run();
547                 if (!(command instanceof OnOffType) && !isHevCycleChannelCommand && !isInfraredChannelCommand) {
548                     getLightStateForCommand().setPowerState(PowerState.ON);
549                 }
550             };
551
552             if (waitForHevCycleDisabled) {
553                 scheduler.schedule(compositeCommandsRunnable, 200, TimeUnit.MILLISECONDS);
554             } else {
555                 compositeCommandsRunnable.run();
556             }
557         }
558     }
559
560     private @Nullable Runnable getChannelCommandRunnable(ChannelUID channelUID, Command command) {
561         switch (channelUID.getId()) {
562             case CHANNEL_ABS_TEMPERATURE:
563             case CHANNEL_TEMPERATURE:
564                 if (command instanceof DecimalType || (command instanceof QuantityType
565                         && ((QuantityType) command).toInvertibleUnit(Units.KELVIN) != null)) {
566                     return () -> handleTemperatureCommand(command);
567                 } else if (command instanceof IncreaseDecreaseType) {
568                     return () -> handleIncreaseDecreaseTemperatureCommand((IncreaseDecreaseType) command);
569                 }
570             case CHANNEL_BRIGHTNESS:
571                 if (command instanceof PercentType) {
572                     return () -> handlePercentCommand((PercentType) command);
573                 } else if (command instanceof OnOffType) {
574                     return () -> handleOnOffCommand((OnOffType) command);
575                 } else if (command instanceof IncreaseDecreaseType) {
576                     return () -> handleIncreaseDecreaseCommand((IncreaseDecreaseType) command);
577                 }
578             case CHANNEL_COLOR:
579                 if (command instanceof HSBType) {
580                     return () -> handleHSBCommand((HSBType) command);
581                 } else if (command instanceof PercentType) {
582                     return () -> handlePercentCommand((PercentType) command);
583                 } else if (command instanceof OnOffType) {
584                     return () -> handleOnOffCommand((OnOffType) command);
585                 } else if (command instanceof IncreaseDecreaseType) {
586                     return () -> handleIncreaseDecreaseCommand((IncreaseDecreaseType) command);
587                 }
588             case CHANNEL_EFFECT:
589                 if (command instanceof StringType && features.hasFeature(TILE_EFFECT)) {
590                     return () -> handleTileEffectCommand((StringType) command);
591                 }
592             case CHANNEL_HEV_CYCLE:
593                 if (command instanceof OnOffType) {
594                     return () -> handleHevCycleCommand((OnOffType) command);
595                 }
596             case CHANNEL_INFRARED:
597                 if (command instanceof PercentType) {
598                     return () -> handleInfraredCommand((PercentType) command);
599                 } else if (command instanceof IncreaseDecreaseType) {
600                     return () -> handleIncreaseDecreaseInfraredCommand((IncreaseDecreaseType) command);
601                 }
602             default:
603                 try {
604                     if (channelUID.getId().startsWith(CHANNEL_ABS_TEMPERATURE_ZONE)) {
605                         int zoneIndex = Integer.parseInt(channelUID.getId().replace(CHANNEL_ABS_TEMPERATURE_ZONE, ""));
606                         if (command instanceof DecimalType || (command instanceof QuantityType
607                                 && ((QuantityType) command).toInvertibleUnit(Units.KELVIN) != null)) {
608                             return () -> handleTemperatureCommand(command, zoneIndex);
609                         }
610                     } else if (channelUID.getId().startsWith(CHANNEL_COLOR_ZONE)) {
611                         int zoneIndex = Integer.parseInt(channelUID.getId().replace(CHANNEL_COLOR_ZONE, ""));
612                         if (command instanceof HSBType) {
613                             return () -> handleHSBCommand((HSBType) command, zoneIndex);
614                         } else if (command instanceof PercentType) {
615                             return () -> handlePercentCommand((PercentType) command, zoneIndex);
616                         } else if (command instanceof IncreaseDecreaseType) {
617                             return () -> handleIncreaseDecreaseCommand((IncreaseDecreaseType) command, zoneIndex);
618                         }
619                     } else if (channelUID.getId().startsWith(CHANNEL_TEMPERATURE_ZONE)) {
620                         int zoneIndex = Integer.parseInt(channelUID.getId().replace(CHANNEL_TEMPERATURE_ZONE, ""));
621                         if (command instanceof PercentType) {
622                             return () -> handleTemperatureCommand((PercentType) command, zoneIndex);
623                         } else if (command instanceof IncreaseDecreaseType) {
624                             return () -> handleIncreaseDecreaseTemperatureCommand((IncreaseDecreaseType) command,
625                                     zoneIndex);
626                         }
627                     }
628                 } catch (NumberFormatException e) {
629                     logger.error("Failed to parse zone index for a command of a light ({}) : {}", logId,
630                             e.getMessage());
631                 }
632         }
633         return null;
634     }
635
636     private LifxLightState getLightStateForCommand() {
637         if (!isStateChangePending()) {
638             pendingLightState.copy(currentLightState);
639         }
640         return pendingLightState;
641     }
642
643     private boolean isStateChangePending() {
644         return pendingLightState.getDurationSinceLastChange().minus(MAX_STATE_CHANGE_DURATION).isNegative();
645     }
646
647     private void handleRefreshCommand(ChannelUID channelUID) {
648         channelStates.remove(channelUID.getId());
649         switch (channelUID.getId()) {
650             case CHANNEL_ABS_TEMPERATURE:
651             case CHANNEL_TEMPERATURE:
652                 sendPacket(new GetRequest());
653                 break;
654             case CHANNEL_COLOR:
655             case CHANNEL_BRIGHTNESS:
656                 sendPacket(new GetLightPowerRequest());
657                 sendPacket(new GetRequest());
658                 break;
659             case CHANNEL_EFFECT:
660                 if (features.hasFeature(TILE_EFFECT)) {
661                     sendPacket(new GetTileEffectRequest());
662                 }
663                 break;
664             case CHANNEL_HEV_CYCLE:
665                 sendPacket(new GetHevCycleRequest());
666                 break;
667             case CHANNEL_INFRARED:
668                 sendPacket(new GetLightInfraredRequest());
669                 break;
670             case CHANNEL_SIGNAL_STRENGTH:
671                 sendPacket(new GetWifiInfoRequest());
672                 break;
673             default:
674                 break;
675         }
676     }
677
678     private void handleTemperatureCommand(Command temperature) {
679         HSBK newColor = getLightStateForCommand().getColor();
680         newColor.setSaturation(PercentType.ZERO);
681         newColor.setKelvin(commandToKelvin(temperature, features.getTemperatureRange()));
682         getLightStateForCommand().setColor(newColor);
683     }
684
685     private void handleTemperatureCommand(Command temperature, int zoneIndex) {
686         HSBK newColor = getLightStateForCommand().getColor(zoneIndex);
687         newColor.setSaturation(PercentType.ZERO);
688         newColor.setKelvin(commandToKelvin(temperature, features.getTemperatureRange()));
689         getLightStateForCommand().setColor(newColor, zoneIndex);
690     }
691
692     private void handleHSBCommand(HSBType hsb) {
693         getLightStateForCommand().setColor(hsb);
694     }
695
696     private void handleHSBCommand(HSBType hsb, int zoneIndex) {
697         getLightStateForCommand().setColor(hsb, zoneIndex);
698     }
699
700     private void handlePercentCommand(PercentType brightness) {
701         getLightStateForCommand().setBrightness(brightness);
702     }
703
704     private void handlePercentCommand(PercentType brightness, int zoneIndex) {
705         getLightStateForCommand().setBrightness(brightness, zoneIndex);
706     }
707
708     private void handleOnOffCommand(OnOffType onOff) {
709         HSBType localPowerOnColor = powerOnColor;
710         if (localPowerOnColor != null && onOff == OnOffType.ON) {
711             getLightStateForCommand().setColor(localPowerOnColor);
712         }
713
714         PercentType localPowerOnTemperature = powerOnTemperature;
715         if (localPowerOnTemperature != null && onOff == OnOffType.ON) {
716             getLightStateForCommand()
717                     .setTemperature(percentTypeToKelvin(localPowerOnTemperature, features.getTemperatureRange()));
718         }
719
720         PercentType powerOnBrightness = this.powerOnBrightness;
721         if (powerOnBrightness != null) {
722             PercentType newBrightness = onOff == OnOffType.ON ? powerOnBrightness : new PercentType(0);
723             getLightStateForCommand().setBrightness(newBrightness);
724         }
725         getLightStateForCommand().setPowerState(onOff);
726     }
727
728     private void handleIncreaseDecreaseCommand(IncreaseDecreaseType increaseDecrease) {
729         HSBK baseColor = getLightStateForCommand().getColor();
730         PercentType newBrightness = increaseDecreasePercentType(increaseDecrease, baseColor.getHSB().getBrightness());
731         handlePercentCommand(newBrightness);
732     }
733
734     private void handleIncreaseDecreaseCommand(IncreaseDecreaseType increaseDecrease, int zoneIndex) {
735         HSBK baseColor = getLightStateForCommand().getColor(zoneIndex);
736         PercentType newBrightness = increaseDecreasePercentType(increaseDecrease, baseColor.getHSB().getBrightness());
737         handlePercentCommand(newBrightness, zoneIndex);
738     }
739
740     private void handleIncreaseDecreaseTemperatureCommand(IncreaseDecreaseType increaseDecrease) {
741         PercentType baseTemperature = kelvinToPercentType(getLightStateForCommand().getColor().getKelvin(),
742                 features.getTemperatureRange());
743         PercentType newTemperature = increaseDecreasePercentType(increaseDecrease, baseTemperature);
744         handleTemperatureCommand(newTemperature);
745     }
746
747     private void handleIncreaseDecreaseTemperatureCommand(IncreaseDecreaseType increaseDecrease, int zoneIndex) {
748         PercentType baseTemperature = kelvinToPercentType(getLightStateForCommand().getColor(zoneIndex).getKelvin(),
749                 features.getTemperatureRange());
750         PercentType newTemperature = increaseDecreasePercentType(increaseDecrease, baseTemperature);
751         handleTemperatureCommand(newTemperature, zoneIndex);
752     }
753
754     private void handleHevCycleCommand(OnOffType onOff) {
755         HevCycleState hevCycleState = new HevCycleState(onOff == OnOffType.ON, hevCycleDuration);
756         getLightStateForCommand().setHevCycleState(hevCycleState);
757     }
758
759     private void handleInfraredCommand(PercentType infrared) {
760         getLightStateForCommand().setInfrared(infrared);
761     }
762
763     private void handleIncreaseDecreaseInfraredCommand(IncreaseDecreaseType increaseDecrease) {
764         PercentType baseInfrared = getLightStateForCommand().getInfrared();
765         if (baseInfrared != null) {
766             PercentType newInfrared = increaseDecreasePercentType(increaseDecrease, baseInfrared);
767             handleInfraredCommand(newInfrared);
768         }
769     }
770
771     private void handleTileEffectCommand(StringType type) {
772         logger.debug("handleTileEffectCommand mode={}", type);
773         Double morphSpeedInMSecs = effectMorphSpeed * 1000.0;
774         Double flameSpeedInMSecs = effectFlameSpeed * 1000.0;
775         try {
776             Effect effect = Effect.createDefault(type.toString(), morphSpeedInMSecs.longValue(),
777                     flameSpeedInMSecs.longValue());
778             getLightStateForCommand().setTileEffect(effect);
779         } catch (IllegalArgumentException e) {
780             logger.debug("{} : Wrong effect type received as command: {}", logId, type);
781         }
782     }
783
784     @Override
785     protected void updateProperties(@Nullable Map<String, String> properties) {
786         String oldHostVersion = getThing().getProperties().get(LifxBindingConstants.PROPERTY_HOST_VERSION);
787         super.updateProperties(properties);
788         String newHostVersion = getThing().getProperties().get(LifxBindingConstants.PROPERTY_HOST_VERSION);
789
790         if (!Objects.equals(oldHostVersion, newHostVersion)) {
791             features.update(getFeatures());
792         }
793     }
794
795     private void updateStateIfChanged(String channel, State newState) {
796         State oldState = channelStates.get(channel);
797         if (oldState == null || !oldState.equals(newState)) {
798             updateState(channel, newState);
799             channelStates.put(channel, newState);
800         }
801     }
802
803     private void updateStatusIfChanged(ThingStatus status) {
804         updateStatusIfChanged(status, ThingStatusDetail.NONE);
805     }
806
807     private void updateStatusIfChanged(ThingStatus status, ThingStatusDetail statusDetail) {
808         ThingStatusInfo newStatusInfo = new ThingStatusInfo(status, statusDetail, null);
809         Duration durationSinceLastUpdate = Duration.between(lastStatusInfoUpdate, LocalDateTime.now());
810         boolean intervalElapsed = MIN_STATUS_INFO_UPDATE_INTERVAL.minus(durationSinceLastUpdate).isNegative();
811
812         ThingStatusInfo oldStatusInfo = statusInfo;
813         if (oldStatusInfo == null || !oldStatusInfo.equals(newStatusInfo) || intervalElapsed) {
814             statusInfo = newStatusInfo;
815             lastStatusInfoUpdate = LocalDateTime.now();
816             updateStatus(status, statusDetail);
817         }
818     }
819 }