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