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