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