/**
* Copyright (c) 2014-2017 by the respective copyright holders.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.smarthome.binding.lifx.handler;
import static org.eclipse.smarthome.binding.lifx.LifxBindingConstants.*;
import static org.eclipse.smarthome.binding.lifx.internal.LifxUtils.increaseDecreasePercentType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
import org.eclipse.smarthome.binding.lifx.LifxBindingConstants;
import org.eclipse.smarthome.binding.lifx.internal.LifxChannelFactory;
import org.eclipse.smarthome.binding.lifx.internal.LifxLightCommunicationHandler;
import org.eclipse.smarthome.binding.lifx.internal.LifxLightCurrentStateUpdater;
import org.eclipse.smarthome.binding.lifx.internal.LifxLightOnlineStateUpdater;
import org.eclipse.smarthome.binding.lifx.internal.LifxLightState;
import org.eclipse.smarthome.binding.lifx.internal.LifxLightStateChanger;
import org.eclipse.smarthome.binding.lifx.internal.fields.HSBK;
import org.eclipse.smarthome.binding.lifx.internal.fields.MACAddress;
import org.eclipse.smarthome.binding.lifx.internal.protocol.GetLightInfraredRequest;
import org.eclipse.smarthome.binding.lifx.internal.protocol.GetLightPowerRequest;
import org.eclipse.smarthome.binding.lifx.internal.protocol.GetRequest;
import org.eclipse.smarthome.binding.lifx.internal.protocol.GetWifiInfoRequest;
import org.eclipse.smarthome.binding.lifx.internal.protocol.Packet;
import org.eclipse.smarthome.binding.lifx.internal.protocol.PowerState;
import org.eclipse.smarthome.binding.lifx.internal.protocol.Products;
import org.eclipse.smarthome.binding.lifx.internal.protocol.SignalStrength;
import org.eclipse.smarthome.config.core.Configuration;
import org.eclipse.smarthome.core.library.types.DecimalType;
import org.eclipse.smarthome.core.library.types.HSBType;
import org.eclipse.smarthome.core.library.types.IncreaseDecreaseType;
import org.eclipse.smarthome.core.library.types.OnOffType;
import org.eclipse.smarthome.core.library.types.PercentType;
import org.eclipse.smarthome.core.thing.Channel;
import org.eclipse.smarthome.core.thing.ChannelUID;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.thing.ThingStatus;
import org.eclipse.smarthome.core.thing.ThingStatusDetail;
import org.eclipse.smarthome.core.thing.binding.BaseThingHandler;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.RefreshType;
import org.eclipse.smarthome.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link LifxLightHandler} is responsible for handling commands, which are
* sent to one of the light channels.
*
* @author Dennis Nobel - Initial contribution
* @author Stefan Bußweiler - Added new thing status handling
* @author Karel Goderis - Rewrite for Firmware V2, and remove dependency on external libraries
* @author Kai Kreuzer - Added configurable transition time and small fixes
* @author Wouter Born - Decomposed class into separate objects
*/
public class LifxLightHandler extends BaseThingHandler {
private Logger logger = LoggerFactory.getLogger(LifxLightHandler.class);
private static final long FADE_TIME_DEFAULT = 300;
private static final int MAX_STATE_CHANGE_DURATION = 4000;
private LifxChannelFactory channelFactory;
private Products product;
private long fadeTime = FADE_TIME_DEFAULT;
private PercentType powerOnBrightness;
private MACAddress macAddress = null;
private String macAsHex;
private ReentrantLock lock = new ReentrantLock();
private Map<String, State> channelStates;
private CurrentLightState currentLightState;
private LifxLightState pendingLightState;
private LifxLightCommunicationHandler communicationHandler;
private LifxLightCurrentStateUpdater currentStateUpdater;
private LifxLightStateChanger lightStateChanger;
private LifxLightOnlineStateUpdater onlineStateUpdater;
public class CurrentLightState extends LifxLightState {
public boolean isOnline() {
return thing.getStatus() == ThingStatus.ONLINE;
}
public boolean isOffline() {
return thing.getStatus() == ThingStatus.OFFLINE;
}
public void setOnline() {
updateStatus(ThingStatus.ONLINE);
}
public void setOffline() {
updateStatus(ThingStatus.OFFLINE);
}
public void setOfflineByCommunicationError() {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
}
@Override
public void setColors(HSBK[] colors) {
if (!isStateChangePending() || isPendingColorStateChangesApplied(getPowerState(), colors)) {
PowerState powerState = isStateChangePending() ? pendingLightState.getPowerState() : getPowerState();
updateColorChannels(powerState, colors);
}
super.setColors(colors);
}
@Override
public void setPowerState(PowerState powerState) {
if (!isStateChangePending() || isPendingColorStateChangesApplied(powerState, getColors())) {
HSBK[] colors = isStateChangePending() ? pendingLightState.getColors() : getColors();
updateColorChannels(powerState, colors);
}
super.setPowerState(powerState);
}
private boolean isPendingColorStateChangesApplied(PowerState powerState, HSBK[] colors) {
return powerState != null && powerState.equals(pendingLightState.getPowerState())
&& Arrays.equals(colors, pendingLightState.getColors());
}
private void updateColorChannels(PowerState powerState, HSBK[] colors) {
HSBK color = colors != null && colors.length > 0 ? colors[0] : null;
HSBK updateColor = nullSafeUpdateColor(powerState, color);
HSBType hsb = updateColor.getHSB();
updateStateIfChanged(CHANNEL_COLOR, hsb);
updateStateIfChanged(CHANNEL_BRIGHTNESS, hsb.getBrightness());
updateStateIfChanged(CHANNEL_TEMPERATURE, updateColor.getTemperature());
updateZoneChannels(powerState, colors);
}
private HSBK nullSafeUpdateColor(PowerState powerState, HSBK color) {
HSBK updateColor = color != null ? color : DEFAULT_COLOR;
if (powerState == PowerState.OFF) {
updateColor = new HSBK(updateColor);
updateColor.setBrightness(PercentType.ZERO);
}
return updateColor;
}
@Override
public void setInfrared(PercentType infrared) {
if (!isStateChangePending() || infrared.equals(pendingLightState.getInfrared())) {
updateStateIfChanged(CHANNEL_INFRARED, infrared);
}
super.setInfrared(infrared);
}
@Override
public void setSignalStrength(SignalStrength signalStrength) {
updateStateIfChanged(CHANNEL_SIGNAL_STRENGTH, new DecimalType(signalStrength.toQualityRating()));
super.setSignalStrength(signalStrength);
}
private void updateZoneChannels(PowerState powerState, HSBK[] colors) {
if (!product.isMultiZone() || colors == null || colors.length == 0) {
return;
}
int oldZones = getColors() != null ? getColors().length : 0;
int newZones = colors.length;
if (oldZones != newZones) {
addRemoveZoneChannels(newZones);
}
for (int i = 0; i < colors.length; i++) {
HSBK color = colors[i];
HSBK updateColor = nullSafeUpdateColor(powerState, color);
updateStateIfChanged(CHANNEL_COLOR_ZONE + i, updateColor.getHSB());
updateStateIfChanged(CHANNEL_TEMPERATURE_ZONE + i, updateColor.getTemperature());
}
}
}
public LifxLightHandler(Thing thing, LifxChannelFactory channelFactory) {
super(thing);
this.channelFactory = channelFactory;
}
@Override
public void initialize() {
try {
lock.lock();
product = getProduct();
macAddress = new MACAddress((String) getConfig().get(LifxBindingConstants.CONFIG_PROPERTY_DEVICE_ID), true);
macAsHex = this.macAddress.getHex();
logger.debug("Initializing the LIFX handler for light '{}'.", macAsHex);
fadeTime = getFadeTime();
powerOnBrightness = getPowerOnBrightness();
channelStates = new HashMap<>();
currentLightState = new CurrentLightState();
pendingLightState = new LifxLightState();
communicationHandler = new LifxLightCommunicationHandler(macAddress, currentLightState);
currentStateUpdater = new LifxLightCurrentStateUpdater(macAddress, currentLightState, communicationHandler,
product);
onlineStateUpdater = new LifxLightOnlineStateUpdater(macAddress, currentLightState, communicationHandler);
lightStateChanger = new LifxLightStateChanger(macAddress, pendingLightState, communicationHandler, product,
fadeTime);
communicationHandler.start();
currentStateUpdater.start();
onlineStateUpdater.start();
lightStateChanger.start();
startOrStopSignalStrengthUpdates();
} catch (Exception e) {
logger.debug("Error occurred while initializing LIFX handler: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
} finally {
lock.unlock();
}
}
@Override
public void dispose() {
try {
lock.lock();
if (communicationHandler != null) {
communicationHandler.stop();
communicationHandler = null;
}
if (currentStateUpdater != null) {
currentStateUpdater.stop();
currentStateUpdater = null;
}
if (onlineStateUpdater != null) {
onlineStateUpdater.stop();
onlineStateUpdater = null;
}
if (lightStateChanger != null) {
lightStateChanger.stop();
lightStateChanger = null;
}
currentLightState = null;
pendingLightState = null;
} finally {
lock.unlock();
}
}
private long getFadeTime() {
Object fadeCfg = getConfig().get(LifxBindingConstants.CONFIG_PROPERTY_FADETIME);
if (fadeCfg == null) {
return FADE_TIME_DEFAULT;
}
try {
return Long.parseLong(fadeCfg.toString());
} catch (NumberFormatException e) {
logger.warn("Invalid value '{}' for transition time, using default instead.", fadeCfg.toString());
return FADE_TIME_DEFAULT;
}
}
private PercentType getPowerOnBrightness() {
Channel channel = null;
if (product.isColor()) {
ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_COLOR);
channel = getThing().getChannel(channelUID.getId());
} else {
ChannelUID channelUID = new ChannelUID(getThing().getUID(), LifxBindingConstants.CHANNEL_BRIGHTNESS);
channel = getThing().getChannel(channelUID.getId());
}
if (channel == null) {
return null;
}
Configuration configuration = channel.getConfiguration();
Object powerOnBrightness = configuration.get(LifxBindingConstants.CONFIG_PROPERTY_POWER_ON_BRIGHTNESS);
return powerOnBrightness == null ? null : new PercentType(powerOnBrightness.toString());
}
private Products getProduct() {
String propertyValue = getThing().getProperties().get(LifxBindingConstants.PROPERTY_PRODUCT_ID);
try {
long productID = Long.parseLong(propertyValue);
return Products.getProductFromProductID(productID);
} catch (IllegalArgumentException e) {
return Products.getLikelyProduct(getThing().getThingTypeUID());
}
}
private void addRemoveZoneChannels(int zones) {
List<Channel> newChannels = new ArrayList<Channel>();
// retain non-zone channels
for (Channel channel : getThing().getChannels()) {
String channelId = channel.getUID().getId();
if (!channelId.startsWith(CHANNEL_COLOR_ZONE) && !channelId.startsWith(CHANNEL_TEMPERATURE_ZONE)) {
newChannels.add(channel);
}
}
// add zone channels
for (int i = 0; i < zones; i++) {
newChannels.add(channelFactory.createColorZoneChannel(getThing().getUID(), i));
newChannels.add(channelFactory.createTemperatureZoneChannel(getThing().getUID(), i));
}
updateThing(editThing().withChannels(newChannels).build());
Map<String, String> properties = editProperties();
properties.put(LifxBindingConstants.PROPERTY_ZONES, Integer.toString(zones));
updateProperties(properties);
}
@Override
public void channelLinked(ChannelUID channelUID) {
super.channelLinked(channelUID);
startOrStopSignalStrengthUpdates();
}
@Override
public void channelUnlinked(ChannelUID channelUID) {
startOrStopSignalStrengthUpdates();
}
private void startOrStopSignalStrengthUpdates() {
currentStateUpdater.setUpdateSignalStrength(isLinked(CHANNEL_SIGNAL_STRENGTH));
}
private void sendPacket(Packet packet) {
communicationHandler.sendPacket(packet);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
try {
switch (channelUID.getId()) {
case CHANNEL_COLOR:
case CHANNEL_BRIGHTNESS:
sendPacket(new GetLightPowerRequest());
sendPacket(new GetRequest());
break;
case CHANNEL_TEMPERATURE:
sendPacket(new GetRequest());
break;
case CHANNEL_INFRARED:
sendPacket(new GetLightInfraredRequest());
break;
case CHANNEL_SIGNAL_STRENGTH:
sendPacket(new GetWifiInfoRequest());
break;
default:
break;
}
} catch (Exception ex) {
logger.error("Error while refreshing a channel for the light: {}", ex.getMessage(), ex);
}
} else {
try {
boolean supportedCommand = true;
switch (channelUID.getId()) {
case CHANNEL_COLOR:
if (command instanceof HSBType) {
handleHSBCommand((HSBType) command);
} else if (command instanceof PercentType) {
handlePercentCommand((PercentType) command);
} else if (command instanceof OnOffType) {
handleOnOffCommand((OnOffType) command);
} else if (command instanceof IncreaseDecreaseType) {
handleIncreaseDecreaseCommand((IncreaseDecreaseType) command);
} else {
supportedCommand = false;
}
break;
case CHANNEL_BRIGHTNESS:
if (command instanceof PercentType) {
handlePercentCommand((PercentType) command);
} else if (command instanceof OnOffType) {
handleOnOffCommand((OnOffType) command);
} else if (command instanceof IncreaseDecreaseType) {
handleIncreaseDecreaseCommand((IncreaseDecreaseType) command);
} else {
supportedCommand = false;
}
break;
case CHANNEL_TEMPERATURE:
if (command instanceof PercentType) {
handleTemperatureCommand((PercentType) command);
} else if (command instanceof IncreaseDecreaseType) {
handleIncreaseDecreaseTemperatureCommand((IncreaseDecreaseType) command);
} else {
supportedCommand = false;
}
break;
case CHANNEL_INFRARED:
if (command instanceof PercentType) {
handleInfraredCommand((PercentType) command);
} else if (command instanceof IncreaseDecreaseType) {
handleIncreaseDecreaseInfraredCommand((IncreaseDecreaseType) command);
} else {
supportedCommand = false;
}
break;
default:
if (channelUID.getId().startsWith(CHANNEL_COLOR_ZONE)) {
int zoneIndex = Integer.parseInt(channelUID.getId().replace(CHANNEL_COLOR_ZONE, ""));
if (command instanceof HSBType) {
handleHSBCommand((HSBType) command, zoneIndex);
} else if (command instanceof PercentType) {
handlePercentCommand((PercentType) command, zoneIndex);
} else if (command instanceof IncreaseDecreaseType) {
handleIncreaseDecreaseCommand((IncreaseDecreaseType) command, zoneIndex);
} else {
supportedCommand = false;
}
} else if (channelUID.getId().startsWith(CHANNEL_TEMPERATURE_ZONE)) {
int zoneIndex = Integer.parseInt(channelUID.getId().replace(CHANNEL_TEMPERATURE_ZONE, ""));
if (command instanceof PercentType) {
handleTemperatureCommand((PercentType) command, zoneIndex);
} else if (command instanceof IncreaseDecreaseType) {
handleIncreaseDecreaseTemperatureCommand((IncreaseDecreaseType) command, zoneIndex);
} else {
supportedCommand = false;
}
} else {
supportedCommand = false;
}
break;
}
if (supportedCommand && !(command instanceof OnOffType)) {
getLightStateForCommand().setPowerState(PowerState.ON);
}
} catch (Exception ex) {
logger.error("Error while updating light: {}", ex.getMessage(), ex);
}
}
}
private LifxLightState getLightStateForCommand() {
if (!isStateChangePending()) {
pendingLightState.copy(currentLightState);
}
return pendingLightState;
}
private boolean isStateChangePending() {
return pendingLightState.getMillisSinceLastChange() < MAX_STATE_CHANGE_DURATION;
}
private void handleTemperatureCommand(PercentType temperature) {
HSBK newColor = getLightStateForCommand().getNullSafeColor();
newColor.setSaturation(PercentType.ZERO);
newColor.setTemperature(temperature);
getLightStateForCommand().setColor(newColor);
}
private void handleTemperatureCommand(PercentType temperature, int zoneIndex) {
HSBK newColor = getLightStateForCommand().getNullSafeColor(zoneIndex);
newColor.setSaturation(PercentType.ZERO);
newColor.setTemperature(temperature);
getLightStateForCommand().setColor(newColor, zoneIndex);
}
private void handleHSBCommand(HSBType hsb) {
getLightStateForCommand().setColor(hsb);
}
private void handleHSBCommand(HSBType hsb, int zoneIndex) {
getLightStateForCommand().setColor(hsb, zoneIndex);
}
private void handlePercentCommand(PercentType brightness) {
getLightStateForCommand().setBrightness(brightness);
}
private void handlePercentCommand(PercentType brightness, int zoneIndex) {
getLightStateForCommand().setBrightness(brightness, zoneIndex);
}
private void handleOnOffCommand(OnOffType onOff) {
if (powerOnBrightness != null) {
PercentType newBrightness = onOff == OnOffType.ON ? powerOnBrightness : new PercentType(0);
getLightStateForCommand().setBrightness(newBrightness);
}
getLightStateForCommand().setPowerState(onOff);
}
private void handleIncreaseDecreaseCommand(IncreaseDecreaseType increaseDecrease) {
HSBK baseColor = getLightStateForCommand().getNullSafeColor();
PercentType newBrightness = increaseDecreasePercentType(increaseDecrease, baseColor.getHSB().getBrightness());
handlePercentCommand(newBrightness);
}
private void handleIncreaseDecreaseCommand(IncreaseDecreaseType increaseDecrease, int zoneIndex) {
HSBK baseColor = getLightStateForCommand().getNullSafeColor(zoneIndex);
PercentType newBrightness = increaseDecreasePercentType(increaseDecrease, baseColor.getHSB().getBrightness());
handlePercentCommand(newBrightness, zoneIndex);
}
private void handleIncreaseDecreaseTemperatureCommand(IncreaseDecreaseType increaseDecrease) {
PercentType baseTemperature = getLightStateForCommand().getNullSafeColor().getTemperature();
PercentType newTemperature = increaseDecreasePercentType(increaseDecrease, baseTemperature);
handleTemperatureCommand(newTemperature);
}
private void handleIncreaseDecreaseTemperatureCommand(IncreaseDecreaseType increaseDecrease, int zoneIndex) {
PercentType baseTemperature = getLightStateForCommand().getNullSafeColor(zoneIndex).getTemperature();
PercentType newTemperature = increaseDecreasePercentType(increaseDecrease, baseTemperature);
handleTemperatureCommand(newTemperature, zoneIndex);
}
private void handleInfraredCommand(PercentType infrared) {
getLightStateForCommand().setInfrared(infrared);
}
private void handleIncreaseDecreaseInfraredCommand(IncreaseDecreaseType increaseDecrease) {
PercentType baseInfrared = getLightStateForCommand().getInfrared();
if (baseInfrared != null) {
PercentType newInfrared = increaseDecreasePercentType(increaseDecrease, baseInfrared);
handleInfraredCommand(newInfrared);
}
}
private void updateStateIfChanged(String channel, State newState) {
State oldState = channelStates.get(channel);
if (oldState == null || !oldState.equals(newState)) {
updateState(channel, newState);
channelStates.put(channel, newState);
}
}
}