2 * Copyright (c) 2010-2023 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.QuantityType;
65 import org.openhab.core.library.types.StringType;
66 import org.openhab.core.library.unit.Units;
67 import org.openhab.core.thing.Channel;
68 import org.openhab.core.thing.ChannelUID;
69 import org.openhab.core.thing.Thing;
70 import org.openhab.core.thing.ThingStatus;
71 import org.openhab.core.thing.ThingStatusDetail;
72 import org.openhab.core.thing.ThingStatusInfo;
73 import org.openhab.core.thing.binding.BaseThingHandler;
74 import org.openhab.core.types.Command;
75 import org.openhab.core.types.RefreshType;
76 import org.openhab.core.types.State;
77 import org.slf4j.Logger;
78 import org.slf4j.LoggerFactory;
81 * The {@link LifxLightHandler} is responsible for handling commands, which are
82 * sent to one of the light channels.
84 * @author Dennis Nobel - Initial contribution
85 * @author Stefan Bußweiler - Added new thing status handling
86 * @author Karel Goderis - Rewrite for Firmware V2, and remove dependency on external libraries
87 * @author Kai Kreuzer - Added configurable transition time and small fixes
88 * @author Wouter Born - Decomposed class into separate objects
89 * @author Pauli Anttila - Added power on temperature and color features.
92 public class LifxLightHandler extends BaseThingHandler {
94 private final Logger logger = LoggerFactory.getLogger(LifxLightHandler.class);
96 private static final Duration MIN_STATUS_INFO_UPDATE_INTERVAL = Duration.ofSeconds(1);
97 private static final Duration MAX_STATE_CHANGE_DURATION = Duration.ofSeconds(4);
99 private final LifxChannelFactory channelFactory;
100 private @NonNullByDefault({}) Features features;
102 private Duration hevCycleDuration = Duration.ZERO;
103 private @Nullable PercentType powerOnBrightness;
104 private @Nullable HSBType powerOnColor;
105 private @Nullable PercentType powerOnTemperature;
106 private Double effectMorphSpeed = 3.0;
107 private Double effectFlameSpeed = 4.0;
109 private @NonNullByDefault({}) String logId;
111 private final ReentrantLock lock = new ReentrantLock();
113 private @NonNullByDefault({}) CurrentLightState currentLightState;
114 private @NonNullByDefault({}) LifxLightState pendingLightState;
116 private Map<String, @Nullable State> channelStates = new HashMap<>();
117 private @Nullable ThingStatusInfo statusInfo;
118 private LocalDateTime lastStatusInfoUpdate = LocalDateTime.MIN;
120 private @NonNullByDefault({}) LifxLightCommunicationHandler communicationHandler;
121 private @NonNullByDefault({}) LifxLightCurrentStateUpdater currentStateUpdater;
122 private @NonNullByDefault({}) LifxLightStateChanger lightStateChanger;
123 private @NonNullByDefault({}) LifxLightOnlineStateUpdater onlineStateUpdater;
124 private @NonNullByDefault({}) LifxLightPropertiesUpdater propertiesUpdater;
126 public class CurrentLightState extends LifxLightState {
128 public boolean isOnline() {
129 return thing.getStatus() == ThingStatus.ONLINE;
132 public boolean isOffline() {
133 return thing.getStatus() == ThingStatus.OFFLINE;
136 public void setOnline() {
137 updateStatusIfChanged(ThingStatus.ONLINE);
140 public void setOnline(MACAddress macAddress) {
141 updateStatusIfChanged(ThingStatus.ONLINE);
142 Configuration configuration = editConfiguration();
143 configuration.put(LifxBindingConstants.CONFIG_PROPERTY_DEVICE_ID, macAddress.getAsLabel());
144 updateConfiguration(configuration);
147 public void setOffline() {
148 updateStatusIfChanged(ThingStatus.OFFLINE);
151 public void setOfflineByCommunicationError() {
152 updateStatusIfChanged(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
156 public void setColors(HSBK[] colors) {
157 if (!isStateChangePending() || isPendingColorStateChangesApplied(getPowerState(), colors)) {
158 PowerState powerState = isStateChangePending() ? pendingLightState.getPowerState() : getPowerState();
159 updateColorChannels(powerState, colors);
161 super.setColors(colors);
165 public void setPowerState(PowerState powerState) {
166 if (!isStateChangePending() || isPendingColorStateChangesApplied(powerState, getColors())) {
167 HSBK[] colors = isStateChangePending() ? pendingLightState.getColors() : getColors();
168 updateColorChannels(powerState, colors);
170 super.setPowerState(powerState);
173 private boolean isPendingColorStateChangesApplied(@Nullable PowerState powerState, HSBK[] colors) {
174 return powerState != null && powerState.equals(pendingLightState.getPowerState())
175 && Arrays.equals(colors, pendingLightState.getColors());
178 private void updateColorChannels(@Nullable PowerState powerState, HSBK[] colors) {
179 HSBK color = colors.length > 0 ? colors[0] : null;
180 HSBK updateColor = nullSafeUpdateColor(powerState, color);
181 HSBType hsb = updateColor.getHSB();
183 updateStateIfChanged(CHANNEL_COLOR, hsb);
184 updateStateIfChanged(CHANNEL_BRIGHTNESS, hsb.getBrightness());
185 updateStateIfChanged(CHANNEL_TEMPERATURE,
186 kelvinToPercentType(updateColor.getKelvin(), features.getTemperatureRange()));
187 updateStateIfChanged(CHANNEL_ABS_TEMPERATURE, new QuantityType(updateColor.getKelvin(), Units.KELVIN));
189 updateZoneChannels(powerState, colors);
192 private HSBK nullSafeUpdateColor(@Nullable PowerState powerState, @Nullable HSBK color) {
193 HSBK updateColor = color != null ? color : DEFAULT_COLOR;
194 if (powerState == PowerState.OFF) {
195 updateColor = new HSBK(updateColor);
196 updateColor.setBrightness(PercentType.ZERO);
202 public void setHevCycleState(HevCycleState hevCycleState) {
203 if (!isStateChangePending() || hevCycleState.equals(pendingLightState.getHevCycleState())) {
204 updateStateIfChanged(CHANNEL_HEV_CYCLE, OnOffType.from(hevCycleState.isEnable()));
206 super.setHevCycleState(hevCycleState);
210 public void setInfrared(PercentType infrared) {
211 if (!isStateChangePending() || infrared.equals(pendingLightState.getInfrared())) {
212 updateStateIfChanged(CHANNEL_INFRARED, infrared);
214 super.setInfrared(infrared);
218 public void setSignalStrength(SignalStrength signalStrength) {
219 updateStateIfChanged(CHANNEL_SIGNAL_STRENGTH, new DecimalType(signalStrength.toQualityRating()));
220 super.setSignalStrength(signalStrength);
224 public void setTileEffect(Effect effect) {
225 updateStateIfChanged(CHANNEL_EFFECT, new StringType(effect.getType().stringValue()));
226 super.setTileEffect(effect);
229 private void updateZoneChannels(@Nullable PowerState powerState, HSBK[] colors) {
230 if (!features.hasFeature(MULTIZONE) || colors.length == 0) {
234 int oldZones = getColors().length;
235 int newZones = colors.length;
236 if (oldZones != newZones) {
237 addRemoveZoneChannels(newZones);
240 for (int i = 0; i < colors.length; i++) {
241 HSBK color = colors[i];
242 HSBK updateColor = nullSafeUpdateColor(powerState, color);
243 updateStateIfChanged(CHANNEL_COLOR_ZONE + i, updateColor.getHSB());
244 updateStateIfChanged(CHANNEL_TEMPERATURE_ZONE + i,
245 kelvinToPercentType(updateColor.getKelvin(), features.getTemperatureRange()));
246 updateStateIfChanged(CHANNEL_ABS_TEMPERATURE_ZONE + i,
247 new QuantityType(updateColor.getKelvin(), Units.KELVIN));
252 public LifxLightHandler(Thing thing, LifxChannelFactory channelFactory) {
254 this.channelFactory = channelFactory;
258 public void initialize() {
262 LifxLightConfig configuration = getConfigAs(LifxLightConfig.class);
264 logId = getLogId(configuration.getMACAddress(), configuration.getHost());
266 if (logger.isDebugEnabled()) {
267 logger.debug("{} : Initializing handler for product {}", logId, getProduct().getName());
270 features = getFeatures();
272 powerOnBrightness = getPowerOnBrightness();
273 powerOnColor = getPowerOnColor();
274 powerOnTemperature = getPowerOnTemperature();
275 Double speed = getEffectSpeed(LifxBindingConstants.CONFIG_PROPERTY_EFFECT_MORPH_SPEED);
277 effectMorphSpeed = speed;
279 speed = getEffectSpeed(LifxBindingConstants.CONFIG_PROPERTY_EFFECT_FLAME_SPEED);
281 effectFlameSpeed = speed;
283 hevCycleDuration = getHevCycleDuration();
285 channelStates.clear();
286 currentLightState = new CurrentLightState();
287 pendingLightState = new LifxLightState();
289 LifxLightContext context = new LifxLightContext(logId, features, configuration, currentLightState,
290 pendingLightState, scheduler);
292 communicationHandler = new LifxLightCommunicationHandler(context);
293 currentStateUpdater = new LifxLightCurrentStateUpdater(context, communicationHandler);
294 onlineStateUpdater = new LifxLightOnlineStateUpdater(context, communicationHandler);
295 propertiesUpdater = new LifxLightPropertiesUpdater(context, communicationHandler);
296 propertiesUpdater.addPropertiesUpdateListener(this::updateProperties);
297 lightStateChanger = new LifxLightStateChanger(context, communicationHandler);
299 if (configuration.getMACAddress() != null || configuration.getHost() != null) {
300 communicationHandler.start();
301 currentStateUpdater.start();
302 onlineStateUpdater.start();
303 propertiesUpdater.start();
304 lightStateChanger.start();
305 startOrStopSignalStrengthUpdates();
307 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
308 "Configure a Device ID or Host");
310 } catch (Exception e) {
311 logger.debug("{} : Error occurred while initializing handler: {}", logId, e.getMessage(), e);
312 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
319 public void dispose() {
323 logger.debug("{} : Disposing handler", logId);
325 if (communicationHandler != null) {
326 communicationHandler.stop();
327 communicationHandler = null;
330 if (currentStateUpdater != null) {
331 currentStateUpdater.stop();
332 currentStateUpdater = null;
335 if (onlineStateUpdater != null) {
336 onlineStateUpdater.stop();
337 onlineStateUpdater = null;
340 if (propertiesUpdater != null) {
341 propertiesUpdater.stop();
342 propertiesUpdater.removePropertiesUpdateListener(this::updateProperties);
343 propertiesUpdater = null;
346 if (lightStateChanger != null) {
347 lightStateChanger.stop();
348 lightStateChanger = null;
351 currentLightState = null;
352 pendingLightState = null;
358 public String getLogId(@Nullable MACAddress macAddress, @Nullable InetSocketAddress host) {
359 return (macAddress != null ? macAddress.getHex() : (host != null ? host.getHostString() : "Unknown"));
362 private @Nullable PercentType getPowerOnBrightness() {
363 Channel channel = null;
365 if (features.hasFeature(COLOR)) {
366 ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_COLOR);
367 channel = getThing().getChannel(channelUID.getId());
369 ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_BRIGHTNESS);
370 channel = getThing().getChannel(channelUID.getId());
373 if (channel == null) {
377 Configuration configuration = channel.getConfiguration();
378 Object powerOnBrightness = configuration.get(LifxBindingConstants.CONFIG_PROPERTY_POWER_ON_BRIGHTNESS);
379 return powerOnBrightness == null ? null : new PercentType(powerOnBrightness.toString());
382 private @Nullable HSBType getPowerOnColor() {
383 Channel channel = null;
385 if (features.hasFeature(COLOR)) {
386 ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_COLOR);
387 channel = getThing().getChannel(channelUID.getId());
390 if (channel == null) {
394 Configuration configuration = channel.getConfiguration();
395 Object powerOnColor = configuration.get(LifxBindingConstants.CONFIG_PROPERTY_POWER_ON_COLOR);
396 return powerOnColor == null ? null : new HSBType(powerOnColor.toString());
399 private @Nullable PercentType getPowerOnTemperature() {
400 ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_TEMPERATURE);
401 Channel channel = getThing().getChannel(channelUID.getId());
403 if (channel == null) {
407 Configuration configuration = channel.getConfiguration();
408 Object powerOnTemperature = configuration.get(LifxBindingConstants.CONFIG_PROPERTY_POWER_ON_TEMPERATURE);
409 if (powerOnTemperature != null) {
410 return new PercentType(powerOnTemperature.toString());
415 private @Nullable Double getEffectSpeed(String parameter) {
416 Channel channel = null;
418 if (features.hasFeature(TILE_EFFECT)) {
419 ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_EFFECT);
420 channel = getThing().getChannel(channelUID.getId());
423 if (channel == null) {
427 Configuration configuration = channel.getConfiguration();
428 Object speed = configuration.get(parameter);
429 return speed == null ? null : Double.valueOf(speed.toString());
432 private Duration getHevCycleDuration() {
433 ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_HEV_CYCLE);
434 Channel channel = getThing().getChannel(channelUID.getId());
436 if (channel == null) {
437 return Duration.ZERO;
440 Configuration configuration = channel.getConfiguration();
441 Object duration = configuration.get(LifxBindingConstants.CONFIG_PROPERTY_HEV_CYCLE_DURATION);
442 return duration == null ? Duration.ZERO : Duration.ofSeconds(Integer.valueOf(duration.toString()));
445 private Features getFeatures() {
446 LifxProduct product = getProduct();
448 String propertyValue = getThing().getProperties().get(LifxBindingConstants.PROPERTY_HOST_VERSION);
449 if (propertyValue == null) {
450 logger.debug("{} : Using features of initial firmware version", logId);
451 return product.getFeatures();
454 logger.debug("{} : Using features of firmware version {}", logId, propertyValue);
455 return product.getFeatures(propertyValue);
458 private LifxProduct getProduct() {
459 String propertyValue = getThing().getProperties().get(LifxBindingConstants.PROPERTY_PRODUCT_ID);
460 if (propertyValue == null) {
461 return LifxProduct.getLikelyProduct(getThing().getThingTypeUID());
464 // Without first conversion to double, on a very first thing creation from discovery inbox,
465 // the product type is incorrectly parsed, as framework passed it as a floating point number
466 // (e.g. 50.0 instead of 50)
467 Double d = Double.valueOf(propertyValue);
468 long productID = d.longValue();
469 return LifxProduct.getProductFromProductID(productID);
470 } catch (IllegalArgumentException e) {
471 return LifxProduct.getLikelyProduct(getThing().getThingTypeUID());
475 private void addRemoveZoneChannels(int zones) {
476 List<Channel> newChannels = new ArrayList<>();
478 // retain non-zone channels
479 for (Channel channel : getThing().getChannels()) {
480 String channelId = channel.getUID().getId();
481 if (!channelId.startsWith(CHANNEL_ABS_TEMPERATURE_ZONE) && !channelId.startsWith(CHANNEL_COLOR_ZONE)
482 && !channelId.startsWith(CHANNEL_TEMPERATURE_ZONE)) {
483 newChannels.add(channel);
488 for (int i = 0; i < zones; i++) {
489 newChannels.add(channelFactory.createColorZoneChannel(getThing().getUID(), i));
490 newChannels.add(channelFactory.createTemperatureZoneChannel(getThing().getUID(), i));
491 newChannels.add(channelFactory.createAbsTemperatureZoneChannel(getThing().getUID(), i));
494 updateThing(editThing().withChannels(newChannels).build());
496 Map<String, String> properties = editProperties();
497 properties.put(LifxBindingConstants.PROPERTY_ZONES, Integer.toString(zones));
498 updateProperties(properties);
502 public void channelLinked(ChannelUID channelUID) {
503 super.channelLinked(channelUID);
504 startOrStopSignalStrengthUpdates();
508 public void channelUnlinked(ChannelUID channelUID) {
509 startOrStopSignalStrengthUpdates();
512 private void startOrStopSignalStrengthUpdates() {
513 currentStateUpdater.setUpdateSignalStrength(isLinked(CHANNEL_SIGNAL_STRENGTH));
516 private void sendPacket(Packet packet) {
517 communicationHandler.sendPacket(packet);
521 public void handleCommand(ChannelUID channelUID, Command command) {
522 if (command instanceof RefreshType) {
523 handleRefreshCommand(channelUID);
525 Runnable channelCommandRunnable = getChannelCommandRunnable(channelUID, command);
526 if (channelCommandRunnable == null) {
530 String channelId = channelUID.getId();
531 boolean isHevCycleChannelCommand = CHANNEL_HEV_CYCLE.equals(channelId);
532 boolean isInfraredChannelCommand = CHANNEL_INFRARED.equals(channelId);
533 boolean waitForHevCycleDisabled = false;
535 if (getFeatures().hasFeature(HEV) && !isHevCycleChannelCommand) {
536 LifxLightState lightState = getLightStateForCommand();
537 HevCycleState currentHevCycleState = lightState.getHevCycleState();
538 if (currentHevCycleState == null || currentHevCycleState.isEnable()) {
539 lightState.setHevCycleState(HevCycleState.OFF);
540 lightState.setPowerState(PowerState.OFF);
541 waitForHevCycleDisabled = true;
545 Runnable compositeCommandsRunnable = () -> {
546 channelCommandRunnable.run();
547 if (!(command instanceof OnOffType) && !isHevCycleChannelCommand && !isInfraredChannelCommand) {
548 getLightStateForCommand().setPowerState(PowerState.ON);
552 if (waitForHevCycleDisabled) {
553 scheduler.schedule(compositeCommandsRunnable, 200, TimeUnit.MILLISECONDS);
555 compositeCommandsRunnable.run();
560 private @Nullable Runnable getChannelCommandRunnable(ChannelUID channelUID, Command command) {
561 switch (channelUID.getId()) {
562 case CHANNEL_ABS_TEMPERATURE:
563 case CHANNEL_TEMPERATURE:
564 if (command instanceof DecimalType || (command instanceof QuantityType quantityCommand
565 && quantityCommand.toInvertibleUnit(Units.KELVIN) != null)) {
566 return () -> handleTemperatureCommand(command);
567 } else if (command instanceof IncreaseDecreaseType increaseDecreaseCommand) {
568 return () -> handleIncreaseDecreaseTemperatureCommand(increaseDecreaseCommand);
570 case CHANNEL_BRIGHTNESS:
571 if (command instanceof PercentType percentCommand) {
572 return () -> handlePercentCommand(percentCommand);
573 } else if (command instanceof OnOffType onOffCommand) {
574 return () -> handleOnOffCommand(onOffCommand);
575 } else if (command instanceof IncreaseDecreaseType increaseDecreaseCommand) {
576 return () -> handleIncreaseDecreaseCommand(increaseDecreaseCommand);
579 if (command instanceof HSBType hsbCommand) {
580 return () -> handleHSBCommand(hsbCommand);
581 } else if (command instanceof PercentType percentCommand) {
582 return () -> handlePercentCommand(percentCommand);
583 } else if (command instanceof OnOffType onOffCommand) {
584 return () -> handleOnOffCommand(onOffCommand);
585 } else if (command instanceof IncreaseDecreaseType increaseDecreaseCommand) {
586 return () -> handleIncreaseDecreaseCommand(increaseDecreaseCommand);
589 if (command instanceof StringType stringCommand && features.hasFeature(TILE_EFFECT)) {
590 return () -> handleTileEffectCommand(stringCommand);
592 case CHANNEL_HEV_CYCLE:
593 if (command instanceof OnOffType onOffCommand) {
594 return () -> handleHevCycleCommand(onOffCommand);
596 case CHANNEL_INFRARED:
597 if (command instanceof PercentType percentCommand) {
598 return () -> handleInfraredCommand(percentCommand);
599 } else if (command instanceof IncreaseDecreaseType increaseDecreaseCommand) {
600 return () -> handleIncreaseDecreaseInfraredCommand(increaseDecreaseCommand);
604 if (channelUID.getId().startsWith(CHANNEL_ABS_TEMPERATURE_ZONE)) {
605 int zoneIndex = Integer.parseInt(channelUID.getId().replace(CHANNEL_ABS_TEMPERATURE_ZONE, ""));
606 if (command instanceof DecimalType || (command instanceof QuantityType quantityCommand
607 && quantityCommand.toInvertibleUnit(Units.KELVIN) != null)) {
608 return () -> handleTemperatureCommand(command, zoneIndex);
610 } else if (channelUID.getId().startsWith(CHANNEL_COLOR_ZONE)) {
611 int zoneIndex = Integer.parseInt(channelUID.getId().replace(CHANNEL_COLOR_ZONE, ""));
612 if (command instanceof HSBType hsbCommand) {
613 return () -> handleHSBCommand(hsbCommand, zoneIndex);
614 } else if (command instanceof PercentType percentCommand) {
615 return () -> handlePercentCommand(percentCommand, zoneIndex);
616 } else if (command instanceof IncreaseDecreaseType increaseDecreaseCommand) {
617 return () -> handleIncreaseDecreaseCommand(increaseDecreaseCommand, zoneIndex);
619 } else if (channelUID.getId().startsWith(CHANNEL_TEMPERATURE_ZONE)) {
620 int zoneIndex = Integer.parseInt(channelUID.getId().replace(CHANNEL_TEMPERATURE_ZONE, ""));
621 if (command instanceof PercentType percentCommand) {
622 return () -> handleTemperatureCommand(percentCommand, zoneIndex);
623 } else if (command instanceof IncreaseDecreaseType increaseDecreaseCommand) {
624 return () -> handleIncreaseDecreaseTemperatureCommand(increaseDecreaseCommand, zoneIndex);
627 } catch (NumberFormatException e) {
628 logger.error("Failed to parse zone index for a command of a light ({}) : {}", logId,
635 private LifxLightState getLightStateForCommand() {
636 if (!isStateChangePending()) {
637 pendingLightState.copy(currentLightState);
639 return pendingLightState;
642 private boolean isStateChangePending() {
643 return pendingLightState.getDurationSinceLastChange().minus(MAX_STATE_CHANGE_DURATION).isNegative();
646 private void handleRefreshCommand(ChannelUID channelUID) {
647 channelStates.remove(channelUID.getId());
648 switch (channelUID.getId()) {
649 case CHANNEL_ABS_TEMPERATURE:
650 case CHANNEL_TEMPERATURE:
651 sendPacket(new GetRequest());
654 case CHANNEL_BRIGHTNESS:
655 sendPacket(new GetLightPowerRequest());
656 sendPacket(new GetRequest());
659 if (features.hasFeature(TILE_EFFECT)) {
660 sendPacket(new GetTileEffectRequest());
663 case CHANNEL_HEV_CYCLE:
664 sendPacket(new GetHevCycleRequest());
666 case CHANNEL_INFRARED:
667 sendPacket(new GetLightInfraredRequest());
669 case CHANNEL_SIGNAL_STRENGTH:
670 sendPacket(new GetWifiInfoRequest());
677 private void handleTemperatureCommand(Command temperature) {
678 HSBK newColor = getLightStateForCommand().getColor();
679 newColor.setSaturation(PercentType.ZERO);
680 newColor.setKelvin(commandToKelvin(temperature, features.getTemperatureRange()));
681 getLightStateForCommand().setColor(newColor);
684 private void handleTemperatureCommand(Command temperature, int zoneIndex) {
685 HSBK newColor = getLightStateForCommand().getColor(zoneIndex);
686 newColor.setSaturation(PercentType.ZERO);
687 newColor.setKelvin(commandToKelvin(temperature, features.getTemperatureRange()));
688 getLightStateForCommand().setColor(newColor, zoneIndex);
691 private void handleHSBCommand(HSBType hsb) {
692 getLightStateForCommand().setColor(hsb);
695 private void handleHSBCommand(HSBType hsb, int zoneIndex) {
696 getLightStateForCommand().setColor(hsb, zoneIndex);
699 private void handlePercentCommand(PercentType brightness) {
700 getLightStateForCommand().setBrightness(brightness);
703 private void handlePercentCommand(PercentType brightness, int zoneIndex) {
704 getLightStateForCommand().setBrightness(brightness, zoneIndex);
707 private void handleOnOffCommand(OnOffType onOff) {
708 HSBType localPowerOnColor = powerOnColor;
709 if (localPowerOnColor != null && onOff == OnOffType.ON) {
710 getLightStateForCommand().setColor(localPowerOnColor);
713 PercentType localPowerOnTemperature = powerOnTemperature;
714 if (localPowerOnTemperature != null && onOff == OnOffType.ON) {
715 getLightStateForCommand()
716 .setTemperature(percentTypeToKelvin(localPowerOnTemperature, features.getTemperatureRange()));
719 PercentType powerOnBrightness = this.powerOnBrightness;
720 if (powerOnBrightness != null) {
721 PercentType newBrightness = onOff == OnOffType.ON ? powerOnBrightness : new PercentType(0);
722 getLightStateForCommand().setBrightness(newBrightness);
724 getLightStateForCommand().setPowerState(onOff);
727 private void handleIncreaseDecreaseCommand(IncreaseDecreaseType increaseDecrease) {
728 HSBK baseColor = getLightStateForCommand().getColor();
729 PercentType newBrightness = increaseDecreasePercentType(increaseDecrease, baseColor.getHSB().getBrightness());
730 handlePercentCommand(newBrightness);
733 private void handleIncreaseDecreaseCommand(IncreaseDecreaseType increaseDecrease, int zoneIndex) {
734 HSBK baseColor = getLightStateForCommand().getColor(zoneIndex);
735 PercentType newBrightness = increaseDecreasePercentType(increaseDecrease, baseColor.getHSB().getBrightness());
736 handlePercentCommand(newBrightness, zoneIndex);
739 private void handleIncreaseDecreaseTemperatureCommand(IncreaseDecreaseType increaseDecrease) {
740 PercentType baseTemperature = kelvinToPercentType(getLightStateForCommand().getColor().getKelvin(),
741 features.getTemperatureRange());
742 PercentType newTemperature = increaseDecreasePercentType(increaseDecrease, baseTemperature);
743 handleTemperatureCommand(newTemperature);
746 private void handleIncreaseDecreaseTemperatureCommand(IncreaseDecreaseType increaseDecrease, int zoneIndex) {
747 PercentType baseTemperature = kelvinToPercentType(getLightStateForCommand().getColor(zoneIndex).getKelvin(),
748 features.getTemperatureRange());
749 PercentType newTemperature = increaseDecreasePercentType(increaseDecrease, baseTemperature);
750 handleTemperatureCommand(newTemperature, zoneIndex);
753 private void handleHevCycleCommand(OnOffType onOff) {
754 HevCycleState hevCycleState = new HevCycleState(onOff == OnOffType.ON, hevCycleDuration);
755 getLightStateForCommand().setHevCycleState(hevCycleState);
758 private void handleInfraredCommand(PercentType infrared) {
759 getLightStateForCommand().setInfrared(infrared);
762 private void handleIncreaseDecreaseInfraredCommand(IncreaseDecreaseType increaseDecrease) {
763 PercentType baseInfrared = getLightStateForCommand().getInfrared();
764 if (baseInfrared != null) {
765 PercentType newInfrared = increaseDecreasePercentType(increaseDecrease, baseInfrared);
766 handleInfraredCommand(newInfrared);
770 private void handleTileEffectCommand(StringType type) {
771 logger.debug("handleTileEffectCommand mode={}", type);
772 Double morphSpeedInMSecs = effectMorphSpeed * 1000.0;
773 Double flameSpeedInMSecs = effectFlameSpeed * 1000.0;
775 Effect effect = Effect.createDefault(type.toString(), morphSpeedInMSecs.longValue(),
776 flameSpeedInMSecs.longValue());
777 getLightStateForCommand().setTileEffect(effect);
778 } catch (IllegalArgumentException e) {
779 logger.debug("{} : Wrong effect type received as command: {}", logId, type);
784 protected void updateProperties(@Nullable Map<String, String> properties) {
785 String oldHostVersion = getThing().getProperties().get(LifxBindingConstants.PROPERTY_HOST_VERSION);
786 super.updateProperties(properties);
787 String newHostVersion = getThing().getProperties().get(LifxBindingConstants.PROPERTY_HOST_VERSION);
789 if (!Objects.equals(oldHostVersion, newHostVersion)) {
790 features.update(getFeatures());
794 private void updateStateIfChanged(String channel, State newState) {
795 State oldState = channelStates.get(channel);
796 if (oldState == null || !oldState.equals(newState)) {
797 updateState(channel, newState);
798 channelStates.put(channel, newState);
802 private void updateStatusIfChanged(ThingStatus status) {
803 updateStatusIfChanged(status, ThingStatusDetail.NONE);
806 private void updateStatusIfChanged(ThingStatus status, ThingStatusDetail statusDetail) {
807 ThingStatusInfo newStatusInfo = new ThingStatusInfo(status, statusDetail, null);
808 Duration durationSinceLastUpdate = Duration.between(lastStatusInfoUpdate, LocalDateTime.now());
809 boolean intervalElapsed = MIN_STATUS_INFO_UPDATE_INTERVAL.minus(durationSinceLastUpdate).isNegative();
811 ThingStatusInfo oldStatusInfo = statusInfo;
812 if (oldStatusInfo == null || !oldStatusInfo.equals(newStatusInfo) || intervalElapsed) {
813 statusInfo = newStatusInfo;
814 lastStatusInfoUpdate = LocalDateTime.now();
815 updateStatus(status, statusDetail);