/**
* 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.hue.handler;
import static org.eclipse.smarthome.binding.hue.HueBindingConstants.*;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.smarthome.binding.hue.internal.FullLight;
import org.eclipse.smarthome.binding.hue.internal.HueBridge;
import org.eclipse.smarthome.binding.hue.internal.State;
import org.eclipse.smarthome.binding.hue.internal.StateUpdate;
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.library.types.StringType;
import org.eclipse.smarthome.core.thing.Bridge;
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.ThingTypeUID;
import org.eclipse.smarthome.core.thing.binding.BaseThingHandler;
import org.eclipse.smarthome.core.thing.binding.ThingHandler;
import org.eclipse.smarthome.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
/**
* {@link HueLightHandler} is the handler for a hue light. It uses the {@link HueBridgeHandler} to execute the actual
* command.
*
* @author Dennis Nobel - Initial contribution of hue binding
* @author Oliver Libutzki
* @author Kai Kreuzer - stabilized code
* @author Andre Fuechsel - implemented switch off when brightness == 0, changed to support generic thing types
* @author Thomas Höfer - added thing properties
* @author Jochen Hiller - fixed status updates for reachable=true/false
* @author Markus Mazurczak - added code for command handling of OSRAM PAR16 50
* bulbs
* @author Yordan Zhelev - added alert and effect functions
* @author Denis Dudnik - switched to internally integrated source of Jue library
*
*/
public class HueLightHandler extends BaseThingHandler implements LightStatusListener {
public final static Set<ThingTypeUID> SUPPORTED_THING_TYPES = Sets.newHashSet(THING_TYPE_COLOR_LIGHT,
THING_TYPE_COLOR_TEMPERATURE_LIGHT, THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_EXTENDED_COLOR_LIGHT,
THING_TYPE_ON_OFF_LIGHT, THING_TYPE_ON_OFF_PLUG, THING_TYPE_DIMMABLE_PLUG);
private final static Map<String, List<String>> VENDOR_MODEL_MAP = new ImmutableMap.Builder<String, List<String>>()
.put("Philips",
Lists.newArrayList("LCT001", "LCT002", "LCT003", "LCT007", "LLC001", "LLC006", "LLC007", "LLC010",
"LLC011", "LLC012", "LLC013", "LLC020", "LST001", "LST002", "LWB004", "LWB006", "LWB007",
"LWL001"))
.put("OSRAM", Lists.newArrayList("Classic_A60_RGBW", "PAR16_50_TW", "Surface_Light_TW", "Plug_01")).build();
private final static String OSRAM_PAR16_50_TW_MODEL_ID = "PAR16_50_TW";
public static final String NORMALIZE_ID_REGEX = "[^a-zA-Z0-9_]";
private String lightId;
private Integer lastSentColorTemp;
private Integer lastSentBrightness;
private Logger logger = LoggerFactory.getLogger(HueLightHandler.class);
// Flag to indicate whether the bulb is of type Osram par16 50 TW or not
private boolean isOsramPar16 = false;
private boolean propertiesInitializedSuccessfully = false;
private HueBridgeHandler bridgeHandler;
ScheduledFuture<?> scheduledFuture;
public HueLightHandler(Thing hueLight) {
super(hueLight);
}
@Override
public void initialize() {
logger.debug("Initializing hue light handler.");
initializeThing((getBridge() == null) ? null : getBridge().getStatus());
}
private void initializeThing(ThingStatus bridgeStatus) {
logger.debug("initializeThing thing {} bridge status {}", getThing().getUID(), bridgeStatus);
final String configLightId = (String) getConfig().get(LIGHT_ID);
if (configLightId != null) {
lightId = configLightId;
// note: this call implicitly registers our handler as a listener on
// the bridge
if (getHueBridgeHandler() != null) {
if (bridgeStatus == ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
initializeProperties();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
} else {
updateStatus(ThingStatus.OFFLINE);
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
}
}
private synchronized void initializeProperties() {
if (!propertiesInitializedSuccessfully) {
FullLight fullLight = getLight();
if (fullLight != null) {
String modelId = fullLight.getModelID().replaceAll(NORMALIZE_ID_REGEX, "_");
updateProperty(Thing.PROPERTY_MODEL_ID, modelId);
updateProperty(Thing.PROPERTY_FIRMWARE_VERSION, fullLight.getSoftwareVersion());
String vendor = getVendor(modelId);
if (vendor != null) {
updateProperty(Thing.PROPERTY_VENDOR, vendor);
}
isOsramPar16 = OSRAM_PAR16_50_TW_MODEL_ID.equals(modelId);
propertiesInitializedSuccessfully = true;
}
}
}
private String getVendor(String modelId) {
for (String vendor : VENDOR_MODEL_MAP.keySet()) {
if (VENDOR_MODEL_MAP.get(vendor).contains(modelId)) {
return vendor;
}
}
return null;
}
@Override
public void dispose() {
logger.debug("Handler disposes. Unregistering listener.");
if (lightId != null) {
HueBridgeHandler bridgeHandler = getHueBridgeHandler();
if (bridgeHandler != null) {
bridgeHandler.unregisterLightStatusListener(this);
this.bridgeHandler = null;
}
lightId = null;
}
}
private FullLight getLight() {
HueBridgeHandler bridgeHandler = getHueBridgeHandler();
if (bridgeHandler != null) {
return bridgeHandler.getLightById(lightId);
}
return null;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
HueBridgeHandler hueBridge = getHueBridgeHandler();
if (hueBridge == null) {
logger.warn("hue bridge handler not found. Cannot handle command without bridge.");
return;
}
FullLight light = getLight();
if (light == null) {
logger.debug("hue light not known on bridge. Cannot handle command.");
return;
}
StateUpdate lightState = null;
switch (channelUID.getId()) {
case CHANNEL_COLORTEMPERATURE:
if (command instanceof PercentType) {
lightState = LightStateConverter.toColorTemperatureLightState((PercentType) command);
} else if (command instanceof OnOffType) {
lightState = LightStateConverter.toOnOffLightState((OnOffType) command);
if (isOsramPar16) {
lightState = addOsramSpecificCommands(lightState, (OnOffType) command);
}
} else if (command instanceof IncreaseDecreaseType) {
lightState = convertColorTempChangeToStateUpdate((IncreaseDecreaseType) command, light);
}
break;
case CHANNEL_BRIGHTNESS:
if (command instanceof PercentType) {
lightState = LightStateConverter.toBrightnessLightState((PercentType) command);
} else if (command instanceof OnOffType) {
lightState = LightStateConverter.toOnOffLightState((OnOffType) command);
if (isOsramPar16) {
lightState = addOsramSpecificCommands(lightState, (OnOffType) command);
}
} else if (command instanceof IncreaseDecreaseType) {
lightState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, light);
}
break;
case CHANNEL_SWITCH:
if (command instanceof OnOffType) {
lightState = LightStateConverter.toOnOffLightState((OnOffType) command);
if (isOsramPar16) {
lightState = addOsramSpecificCommands(lightState, (OnOffType) command);
}
}
break;
case CHANNEL_COLOR:
if (command instanceof HSBType) {
HSBType hsbCommand = (HSBType) command;
if (hsbCommand.getBrightness().intValue() == 0) {
lightState = LightStateConverter.toOnOffLightState(OnOffType.OFF);
} else {
lightState = LightStateConverter.toColorLightState(hsbCommand);
}
} else if (command instanceof PercentType) {
lightState = LightStateConverter.toBrightnessLightState((PercentType) command);
} else if (command instanceof OnOffType) {
lightState = LightStateConverter.toOnOffLightState((OnOffType) command);
} else if (command instanceof IncreaseDecreaseType) {
lightState = convertBrightnessChangeToStateUpdate((IncreaseDecreaseType) command, light);
}
break;
case CHANNEL_ALERT:
if (command instanceof StringType) {
lightState = LightStateConverter.toAlertState((StringType) command);
if (lightState == null) {
// Unsupported StringType is passed. Log a warning
// message and return.
logger.warn("Unsupported String command: {}. Supported commands are: {}, {}, {} ", command,
LightStateConverter.ALERT_MODE_NONE, LightStateConverter.ALERT_MODE_SELECT,
LightStateConverter.ALERT_MODE_LONG_SELECT);
return;
} else {
scheduleAlertStateRestore(command);
}
}
break;
case CHANNEL_EFFECT:
if (command instanceof OnOffType) {
lightState = LightStateConverter.toOnOffEffectState((OnOffType) command);
}
break;
}
if (lightState != null) {
hueBridge.updateLightState(light, lightState);
} else {
logger.warn("Command send to an unknown channel id: " + channelUID);
}
}
/*
* Applies additional {@link StateUpdate} commands as a workaround for Osram
* Lightify PAR16 TW firmware bug. Also see
* http://www.everyhue.com/vanilla/discussion
* /1756/solved-lightify-turning-off
*/
private StateUpdate addOsramSpecificCommands(StateUpdate lightState, OnOffType actionType) {
if (actionType.equals(OnOffType.ON)) {
lightState.setBrightness(254);
} else {
lightState.setTransitionTime(0);
}
return lightState;
}
private StateUpdate convertColorTempChangeToStateUpdate(IncreaseDecreaseType command, FullLight light) {
StateUpdate stateUpdate = null;
Integer currentColorTemp = getCurrentColorTemp(light.getState());
if (currentColorTemp != null) {
int newColorTemp = LightStateConverter.toAdjustedColorTemp(command, currentColorTemp);
stateUpdate = new StateUpdate().setColorTemperature(newColorTemp);
lastSentColorTemp = newColorTemp;
}
return stateUpdate;
}
private Integer getCurrentColorTemp(State lightState) {
Integer colorTemp = lastSentColorTemp;
if (colorTemp == null && lightState != null) {
colorTemp = lightState.getColorTemperature();
}
return colorTemp;
}
private StateUpdate convertBrightnessChangeToStateUpdate(IncreaseDecreaseType command, FullLight light) {
StateUpdate stateUpdate = null;
Integer currentBrightness = getCurrentBrightness(light.getState());
if (currentBrightness != null) {
int newBrightness = LightStateConverter.toAdjustedBrightness(command, currentBrightness);
stateUpdate = createBrightnessStateUpdate(currentBrightness, newBrightness);
lastSentBrightness = newBrightness;
}
return stateUpdate;
}
private Integer getCurrentBrightness(State lightState) {
Integer brightness = lastSentBrightness;
if (brightness == null && lightState != null) {
if (!lightState.isOn()) {
brightness = 0;
} else {
brightness = lightState.getBrightness();
}
}
return brightness;
}
private StateUpdate createBrightnessStateUpdate(int currentBrightness, int newBrightness) {
StateUpdate lightUpdate = new StateUpdate();
if (newBrightness == 0) {
lightUpdate.turnOff();
} else {
lightUpdate.setBrightness(newBrightness);
if (currentBrightness == 0) {
lightUpdate.turnOn();
}
}
return lightUpdate;
}
private synchronized HueBridgeHandler getHueBridgeHandler() {
if (this.bridgeHandler == null) {
Bridge bridge = getBridge();
if (bridge == null) {
return null;
}
ThingHandler handler = bridge.getHandler();
if (handler instanceof HueBridgeHandler) {
this.bridgeHandler = (HueBridgeHandler) handler;
this.bridgeHandler.registerLightStatusListener(this);
} else {
return null;
}
}
return this.bridgeHandler;
}
@Override
public void onLightStateChanged(HueBridge bridge, FullLight fullLight) {
if (fullLight != null && fullLight.getId().equals(lightId)) {
initializeProperties();
lastSentColorTemp = null;
lastSentBrightness = null;
// update status (ONLINE, OFFLINE)
if (fullLight.getState().isReachable()) {
updateStatus(ThingStatus.ONLINE);
} else {
// we assume OFFLINE without any error (NONE), as this is an
// expected state (when bulb powered off)
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Bridge reports light as not reachable");
}
HSBType hsbType = LightStateConverter.toHSBType(fullLight.getState());
if (!fullLight.getState().isOn()) {
hsbType = new HSBType(hsbType.getHue(), hsbType.getSaturation(), new PercentType(0));
}
updateState(CHANNEL_COLOR, hsbType);
PercentType percentType = LightStateConverter.toColorTemperaturePercentType(fullLight.getState());
updateState(CHANNEL_COLORTEMPERATURE, percentType);
percentType = LightStateConverter.toBrightnessPercentType(fullLight.getState());
if (!fullLight.getState().isOn()) {
percentType = new PercentType(0);
}
updateState(CHANNEL_BRIGHTNESS, percentType);
StringType stringType = LightStateConverter.toAlertStringType(fullLight.getState());
if (!stringType.toString().equals("NULL")) {
updateState(CHANNEL_ALERT, stringType);
scheduleAlertStateRestore(stringType);
}
}
}
@Override
public void channelLinked(ChannelUID channelUID) {
HueBridgeHandler handler = getHueBridgeHandler();
if (handler != null) {
onLightStateChanged(null, handler.getLightById(lightId));
}
}
@Override
public void onLightRemoved(HueBridge bridge, FullLight light) {
if (light.getId().equals(lightId)) {
updateStatus(ThingStatus.OFFLINE);
}
}
@Override
public void onLightAdded(HueBridge bridge, FullLight light) {
if (light.getId().equals(lightId)) {
updateStatus(ThingStatus.ONLINE);
onLightStateChanged(bridge, light);
}
}
/**
* Schedules restoration of the alert item state to {@link LightStateConverter#ALERT_MODE_NONE} after a given time.
* <br>
* Based on the initial command:
* <ul>
* <li>For {@link LightStateConverter#ALERT_MODE_SELECT} restoration will be triggered after <strong>2
* seconds</strong>.
* <li>For {@link LightStateConverter#ALERT_MODE_LONG_SELECT} restoration will be triggered after <strong>15
* seconds</strong>.
* </ul>
* This method also cancels any previously scheduled restoration.
*
* @param command
* The {@link Command} sent to the item
*/
private void scheduleAlertStateRestore(Command command) {
cancelScheduledFuture();
int delay = getAlertDuration(command);
if (delay > 0) {
scheduledFuture = scheduler.schedule(new Runnable() {
@Override
public void run() {
updateState(CHANNEL_ALERT, new StringType(LightStateConverter.ALERT_MODE_NONE));
}
}, delay, TimeUnit.MILLISECONDS);
}
}
/**
* This method will cancel previously scheduled alert item state
* restoration.
*/
private void cancelScheduledFuture() {
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
}
/**
* This method returns the time in <strong>milliseconds</strong> after
* which, the state of the alert item has to be restored to {@link LightStateConverter#ALERT_MODE_NONE}.
*
* @param command
* The initial command sent to the alert item.
* @return Based on the initial command will return:
* <ul>
* <li><strong>2000</strong> for {@link LightStateConverter#ALERT_MODE_SELECT}.
* <li><strong>15000</strong> for {@link LightStateConverter#ALERT_MODE_LONG_SELECT}.
* <li><strong>-1</strong> for any command different from the previous two.
* </ul>
*/
private int getAlertDuration(Command command) {
int delay;
switch (command.toString()) {
case LightStateConverter.ALERT_MODE_LONG_SELECT:
delay = 15000;
break;
case LightStateConverter.ALERT_MODE_SELECT:
delay = 2000;
break;
default:
delay = -1;
break;
}
return delay;
}
}