2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.lifx.internal.handler;
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.*;
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;
27 import java.util.Objects;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.locks.ReentrantLock;
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;
79 * The {@link LifxLightHandler} is responsible for handling commands, which are
80 * sent to one of the light channels.
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.
90 public class LifxLightHandler extends BaseThingHandler {
92 private final Logger logger = LoggerFactory.getLogger(LifxLightHandler.class);
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);
97 private final LifxChannelFactory channelFactory;
98 private @NonNullByDefault({}) Features features;
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;
107 private @NonNullByDefault({}) String logId;
109 private final ReentrantLock lock = new ReentrantLock();
111 private @NonNullByDefault({}) CurrentLightState currentLightState;
112 private @NonNullByDefault({}) LifxLightState pendingLightState;
114 private Map<String, @Nullable State> channelStates = new HashMap<>();
115 private @Nullable ThingStatusInfo statusInfo;
116 private LocalDateTime lastStatusInfoUpdate = LocalDateTime.MIN;
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;
124 public class CurrentLightState extends LifxLightState {
126 public boolean isOnline() {
127 return thing.getStatus() == ThingStatus.ONLINE;
130 public boolean isOffline() {
131 return thing.getStatus() == ThingStatus.OFFLINE;
134 public void setOnline() {
135 updateStatusIfChanged(ThingStatus.ONLINE);
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);
145 public void setOffline() {
146 updateStatusIfChanged(ThingStatus.OFFLINE);
149 public void setOfflineByCommunicationError() {
150 updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
154 public void setColors(HSBK[] colors) {
155 if (!isStateChangePending() || isPendingColorStateChangesApplied(getPowerState(), colors)) {
156 PowerState powerState = isStateChangePending() ? pendingLightState.getPowerState() : getPowerState();
157 updateColorChannels(powerState, colors);
159 super.setColors(colors);
163 public void setPowerState(PowerState powerState) {
164 if (!isStateChangePending() || isPendingColorStateChangesApplied(powerState, getColors())) {
165 HSBK[] colors = isStateChangePending() ? pendingLightState.getColors() : getColors();
166 updateColorChannels(powerState, colors);
168 super.setPowerState(powerState);
171 private boolean isPendingColorStateChangesApplied(@Nullable PowerState powerState, HSBK[] colors) {
172 return powerState != null && powerState.equals(pendingLightState.getPowerState())
173 && Arrays.equals(colors, pendingLightState.getColors());
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();
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()));
187 updateZoneChannels(powerState, colors);
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);
200 public void setHevCycleState(HevCycleState hevCycleState) {
201 if (!isStateChangePending() || hevCycleState.equals(pendingLightState.getHevCycleState())) {
202 updateStateIfChanged(CHANNEL_HEV_CYCLE, OnOffType.from(hevCycleState.isEnable()));
204 super.setHevCycleState(hevCycleState);
208 public void setInfrared(PercentType infrared) {
209 if (!isStateChangePending() || infrared.equals(pendingLightState.getInfrared())) {
210 updateStateIfChanged(CHANNEL_INFRARED, infrared);
212 super.setInfrared(infrared);
216 public void setSignalStrength(SignalStrength signalStrength) {
217 updateStateIfChanged(CHANNEL_SIGNAL_STRENGTH, new DecimalType(signalStrength.toQualityRating()));
218 super.setSignalStrength(signalStrength);
222 public void setTileEffect(Effect effect) {
223 updateStateIfChanged(CHANNEL_EFFECT, new StringType(effect.getType().stringValue()));
224 super.setTileEffect(effect);
227 private void updateZoneChannels(@Nullable PowerState powerState, HSBK[] colors) {
228 if (!features.hasFeature(MULTIZONE) || colors.length == 0) {
232 int oldZones = getColors().length;
233 int newZones = colors.length;
234 if (oldZones != newZones) {
235 addRemoveZoneChannels(newZones);
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()));
249 public LifxLightHandler(Thing thing, LifxChannelFactory channelFactory) {
251 this.channelFactory = channelFactory;
255 public void initialize() {
259 LifxLightConfig configuration = getConfigAs(LifxLightConfig.class);
261 logId = getLogId(configuration.getMACAddress(), configuration.getHost());
263 if (logger.isDebugEnabled()) {
264 logger.debug("{} : Initializing handler for product {}", logId, getProduct().getName());
267 features = getFeatures();
269 powerOnBrightness = getPowerOnBrightness();
270 powerOnColor = getPowerOnColor();
271 powerOnTemperature = getPowerOnTemperature();
272 Double speed = getEffectSpeed(LifxBindingConstants.CONFIG_PROPERTY_EFFECT_MORPH_SPEED);
274 effectMorphSpeed = speed;
276 speed = getEffectSpeed(LifxBindingConstants.CONFIG_PROPERTY_EFFECT_FLAME_SPEED);
278 effectFlameSpeed = speed;
280 hevCycleDuration = getHevCycleDuration();
282 channelStates.clear();
283 currentLightState = new CurrentLightState();
284 pendingLightState = new LifxLightState();
286 LifxLightContext context = new LifxLightContext(logId, features, configuration, currentLightState,
287 pendingLightState, scheduler);
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);
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();
304 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
305 "Configure a Device ID or Host");
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());
316 public void dispose() {
320 logger.debug("{} : Disposing handler", logId);
322 if (communicationHandler != null) {
323 communicationHandler.stop();
324 communicationHandler = null;
327 if (currentStateUpdater != null) {
328 currentStateUpdater.stop();
329 currentStateUpdater = null;
332 if (onlineStateUpdater != null) {
333 onlineStateUpdater.stop();
334 onlineStateUpdater = null;
337 if (propertiesUpdater != null) {
338 propertiesUpdater.stop();
339 propertiesUpdater.removePropertiesUpdateListener(this::updateProperties);
340 propertiesUpdater = null;
343 if (lightStateChanger != null) {
344 lightStateChanger.stop();
345 lightStateChanger = null;
348 currentLightState = null;
349 pendingLightState = null;
355 public String getLogId(@Nullable MACAddress macAddress, @Nullable InetSocketAddress host) {
356 return (macAddress != null ? macAddress.getHex() : (host != null ? host.getHostString() : "Unknown"));
359 private @Nullable PercentType getPowerOnBrightness() {
360 Channel channel = null;
362 if (features.hasFeature(COLOR)) {
363 ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_COLOR);
364 channel = getThing().getChannel(channelUID.getId());
366 ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_BRIGHTNESS);
367 channel = getThing().getChannel(channelUID.getId());
370 if (channel == null) {
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());
379 private @Nullable HSBType getPowerOnColor() {
380 Channel channel = null;
382 if (features.hasFeature(COLOR)) {
383 ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_COLOR);
384 channel = getThing().getChannel(channelUID.getId());
387 if (channel == null) {
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());
396 private @Nullable PercentType getPowerOnTemperature() {
397 ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_TEMPERATURE);
398 Channel channel = getThing().getChannel(channelUID.getId());
400 if (channel == null) {
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());
412 private @Nullable Double getEffectSpeed(String parameter) {
413 Channel channel = null;
415 if (features.hasFeature(TILE_EFFECT)) {
416 ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_EFFECT);
417 channel = getThing().getChannel(channelUID.getId());
420 if (channel == null) {
424 Configuration configuration = channel.getConfiguration();
425 Object speed = configuration.get(parameter);
426 return speed == null ? null : Double.valueOf(speed.toString());
429 private Duration getHevCycleDuration() {
430 ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_HEV_CYCLE);
431 Channel channel = getThing().getChannel(channelUID.getId());
433 if (channel == null) {
434 return Duration.ZERO;
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()));
442 private Features getFeatures() {
443 LifxProduct product = getProduct();
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();
451 logger.debug("{} : Using features of firmware version {}", logId, propertyValue);
452 return product.getFeatures(propertyValue);
455 private LifxProduct getProduct() {
456 String propertyValue = getThing().getProperties().get(LifxBindingConstants.PROPERTY_PRODUCT_ID);
457 if (propertyValue == null) {
458 return LifxProduct.getLikelyProduct(getThing().getThingTypeUID());
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());
472 private void addRemoveZoneChannels(int zones) {
473 List<Channel> newChannels = new ArrayList<>();
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);
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));
491 updateThing(editThing().withChannels(newChannels).build());
493 Map<String, String> properties = editProperties();
494 properties.put(LifxBindingConstants.PROPERTY_ZONES, Integer.toString(zones));
495 updateProperties(properties);
499 public void channelLinked(ChannelUID channelUID) {
500 super.channelLinked(channelUID);
501 startOrStopSignalStrengthUpdates();
505 public void channelUnlinked(ChannelUID channelUID) {
506 startOrStopSignalStrengthUpdates();
509 private void startOrStopSignalStrengthUpdates() {
510 currentStateUpdater.setUpdateSignalStrength(isLinked(CHANNEL_SIGNAL_STRENGTH));
513 private void sendPacket(Packet packet) {
514 communicationHandler.sendPacket(packet);
518 public void handleCommand(ChannelUID channelUID, Command command) {
519 if (command instanceof RefreshType) {
520 handleRefreshCommand(channelUID);
522 Runnable channelCommandRunnable = getChannelCommandRunnable(channelUID, command);
523 if (channelCommandRunnable == null) {
527 String channelId = channelUID.getId();
528 boolean isHevCycleChannelCommand = CHANNEL_HEV_CYCLE.equals(channelId);
529 boolean isInfraredChannelCommand = CHANNEL_INFRARED.equals(channelId);
530 boolean waitForHevCycleDisabled = false;
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;
542 Runnable compositeCommandsRunnable = () -> {
543 channelCommandRunnable.run();
544 if (!(command instanceof OnOffType) && !isHevCycleChannelCommand && !isInfraredChannelCommand) {
545 getLightStateForCommand().setPowerState(PowerState.ON);
549 if (waitForHevCycleDisabled) {
550 scheduler.schedule(compositeCommandsRunnable, 200, TimeUnit.MILLISECONDS);
552 compositeCommandsRunnable.run();
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);
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);
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);
585 if (command instanceof StringType && features.hasFeature(TILE_EFFECT)) {
586 return () -> handleTileEffectCommand((StringType) command);
588 case CHANNEL_HEV_CYCLE:
589 if (command instanceof OnOffType) {
590 return () -> handleHevCycleCommand((OnOffType) command);
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);
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);
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);
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,
623 } catch (NumberFormatException e) {
624 logger.error("Failed to parse zone index for a command of a light ({}) : {}", logId,
631 private LifxLightState getLightStateForCommand() {
632 if (!isStateChangePending()) {
633 pendingLightState.copy(currentLightState);
635 return pendingLightState;
638 private boolean isStateChangePending() {
639 return pendingLightState.getDurationSinceLastChange().minus(MAX_STATE_CHANGE_DURATION).isNegative();
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());
650 case CHANNEL_BRIGHTNESS:
651 sendPacket(new GetLightPowerRequest());
652 sendPacket(new GetRequest());
655 if (features.hasFeature(TILE_EFFECT)) {
656 sendPacket(new GetTileEffectRequest());
659 case CHANNEL_HEV_CYCLE:
660 sendPacket(new GetHevCycleRequest());
662 case CHANNEL_INFRARED:
663 sendPacket(new GetLightInfraredRequest());
665 case CHANNEL_SIGNAL_STRENGTH:
666 sendPacket(new GetWifiInfoRequest());
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);
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);
687 private void handleHSBCommand(HSBType hsb) {
688 getLightStateForCommand().setColor(hsb);
691 private void handleHSBCommand(HSBType hsb, int zoneIndex) {
692 getLightStateForCommand().setColor(hsb, zoneIndex);
695 private void handlePercentCommand(PercentType brightness) {
696 getLightStateForCommand().setBrightness(brightness);
699 private void handlePercentCommand(PercentType brightness, int zoneIndex) {
700 getLightStateForCommand().setBrightness(brightness, zoneIndex);
703 private void handleOnOffCommand(OnOffType onOff) {
704 HSBType localPowerOnColor = powerOnColor;
705 if (localPowerOnColor != null && onOff == OnOffType.ON) {
706 getLightStateForCommand().setColor(localPowerOnColor);
709 PercentType localPowerOnTemperature = powerOnTemperature;
710 if (localPowerOnTemperature != null && onOff == OnOffType.ON) {
711 getLightStateForCommand()
712 .setTemperature(percentTypeToKelvin(localPowerOnTemperature, features.getTemperatureRange()));
715 PercentType powerOnBrightness = this.powerOnBrightness;
716 if (powerOnBrightness != null) {
717 PercentType newBrightness = onOff == OnOffType.ON ? powerOnBrightness : new PercentType(0);
718 getLightStateForCommand().setBrightness(newBrightness);
720 getLightStateForCommand().setPowerState(onOff);
723 private void handleIncreaseDecreaseCommand(IncreaseDecreaseType increaseDecrease) {
724 HSBK baseColor = getLightStateForCommand().getColor();
725 PercentType newBrightness = increaseDecreasePercentType(increaseDecrease, baseColor.getHSB().getBrightness());
726 handlePercentCommand(newBrightness);
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);
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);
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);
749 private void handleHevCycleCommand(OnOffType onOff) {
750 HevCycleState hevCycleState = new HevCycleState(onOff == OnOffType.ON, hevCycleDuration);
751 getLightStateForCommand().setHevCycleState(hevCycleState);
754 private void handleInfraredCommand(PercentType infrared) {
755 getLightStateForCommand().setInfrared(infrared);
758 private void handleIncreaseDecreaseInfraredCommand(IncreaseDecreaseType increaseDecrease) {
759 PercentType baseInfrared = getLightStateForCommand().getInfrared();
760 if (baseInfrared != null) {
761 PercentType newInfrared = increaseDecreasePercentType(increaseDecrease, baseInfrared);
762 handleInfraredCommand(newInfrared);
766 private void handleTileEffectCommand(StringType type) {
767 logger.debug("handleTileEffectCommand mode={}", type);
768 Double morphSpeedInMSecs = effectMorphSpeed * 1000.0;
769 Double flameSpeedInMSecs = effectFlameSpeed * 1000.0;
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);
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);
785 if (!Objects.equals(oldHostVersion, newHostVersion)) {
786 features.update(getFeatures());
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);
798 private void updateStatusIfChanged(ThingStatus status) {
799 updateStatusIfChanged(status, ThingStatusDetail.NONE);
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();
807 ThingStatusInfo oldStatusInfo = statusInfo;
808 if (oldStatusInfo == null || !oldStatusInfo.equals(newStatusInfo) || intervalElapsed) {
809 statusInfo = newStatusInfo;
810 lastStatusInfoUpdate = LocalDateTime.now();
811 updateStatus(status, statusDetail);