diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBridge.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBridge.java index 99b7db7d115a1..cb60b099bbce9 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBridge.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBridge.java @@ -28,6 +28,7 @@ import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ScheduledExecutorService; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -851,6 +852,27 @@ public void deleteSchedule(Schedule schedule) throws IOException, ApiException { handleErrors(result); } + /** + * Returns the list of scenes that are not recyclable. + * + * @return all scenes that can be activated + */ + public List getScenes() throws IOException, ApiException { + requireAuthentication(); + + Result result = http.get(getRelativeURL("scenes")); + handleErrors(result); + + Map sceneMap = safeFromJson(result.getBody(), Scene.GSON_TYPE); + return sceneMap.entrySet().parallelStream()// + .map(e -> { + e.getValue().setId(e.getKey()); + return e.getValue(); + })// + .filter(scene -> !scene.isRecycle())// + .collect(Collectors.toList()); + } + /** * Activate scene to all lights that belong to the scene. * diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueThingHandlerFactory.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueThingHandlerFactory.java index 2073cf59d57fc..7e8d84720a707 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueThingHandlerFactory.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueThingHandlerFactory.java @@ -37,6 +37,7 @@ import org.openhab.binding.hue.internal.handler.HueBridgeHandler; import org.openhab.binding.hue.internal.handler.HueGroupHandler; import org.openhab.binding.hue.internal.handler.HueLightHandler; +import org.openhab.binding.hue.internal.handler.HueStateDescriptionOptionProvider; import org.openhab.binding.hue.internal.handler.sensors.ClipHandler; import org.openhab.binding.hue.internal.handler.sensors.DimmerSwitchHandler; import org.openhab.binding.hue.internal.handler.sensors.LightLevelHandler; @@ -45,6 +46,7 @@ import org.openhab.binding.hue.internal.handler.sensors.TemperatureHandler; import org.osgi.framework.ServiceRegistration; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; /** * {@link HueThingHandlerFactory} is a factory for {@link HueBridgeHandler}s. @@ -69,6 +71,9 @@ public class HueThingHandlerFactory extends BaseThingHandlerFactory { private final Map> discoveryServiceRegs = new HashMap<>(); + @NonNullByDefault({}) + private HueStateDescriptionOptionProvider stateOptionProvider; + @Override public @Nullable Thing createThing(ThingTypeUID thingTypeUID, Configuration configuration, @Nullable ThingUID thingUID, @Nullable ThingUID bridgeUID) { @@ -136,7 +141,7 @@ private ThingUID getThingUID(ThingTypeUID thingTypeUID, String id, @Nullable Thi @Override protected @Nullable ThingHandler createHandler(Thing thing) { if (HueBridgeHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { - HueBridgeHandler handler = new HueBridgeHandler((Bridge) thing); + HueBridgeHandler handler = new HueBridgeHandler((Bridge) thing, stateOptionProvider); registerLightDiscoveryService(handler); return handler; } else if (HueLightHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) { @@ -182,4 +187,13 @@ protected synchronized void removeHandler(ThingHandler thingHandler) { } } } + + @Reference + protected void setHueStateDescriptionOptionProvider(HueStateDescriptionOptionProvider stateOptionProvider) { + this.stateOptionProvider = stateOptionProvider; + } + + protected void unsetHueStateDescriptionOptionProvider(HueStateDescriptionOptionProvider stateOptionProvider) { + this.stateOptionProvider = null; + } } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/Scene.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/Scene.java new file mode 100644 index 0000000000000..b60a66f21f0b9 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/Scene.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.smarthome.core.types.StateOption; + +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; + +/** + * Basic scene information. + * + * @author Hengrui Jiang - Initial contribution + */ +public class Scene { + public static final Type GSON_TYPE = new TypeToken>() { + }.getType(); + + private String id; + private String name; + @SerializedName("lights") + private List lightIds; + @SerializedName("group") + private String groupId; + private boolean recycle; + + @NonNull + public String getId() { + return id; + } + + void setId(String id) { + this.id = id; + } + + /** + * Returns the human readable name of the scene. If the name is omitted upon creation, this + * defaults to the ID. + * + * @return human readable name of the scene + */ + @NonNull + public String getName() { + return name; + } + + /** + * Returns the list of lights that the scene applies to. For group scenes, this list should be identical to the list + * of all lights that are in the group. + * + * @return list of lights that the scene applies to + */ + @NonNull + public List getLightIds() { + return lightIds; + } + + /** + * Returns the group that the scene belongs to. This field is optional for scenes that applies to a specific list of + * lights instead of a group. + * + * @return the group that the scene belongs to + */ + public String getGroupId() { + return groupId; + } + + /** + * Indicates if the scene can be recycled by the bridge. A recyclable scene is not able to be activated. + * + * @return whether the scene can be recycled + */ + public boolean isRecycle() { + return recycle; + } + + public StateOption toStateOption(Map groupNames) { + StringBuilder stateOptionLabel = new StringBuilder(name); + if (groupId != null && groupNames.containsKey(groupId)) { + stateOptionLabel.append(" (").append(groupNames.get(groupId)).append(")"); + } + if (!id.contentEquals(name)) { + stateOptionLabel.append(" [").append(id).append("]"); + } + + return new StateOption(id, stateOptionLabel.toString()); + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java index a055de123182d..a903ce614429c 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java @@ -30,6 +30,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -45,6 +46,7 @@ import org.eclipse.smarthome.core.thing.ThingTypeUID; import org.eclipse.smarthome.core.thing.binding.ConfigStatusBridgeHandler; import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.StateOption; import org.openhab.binding.hue.internal.ApiVersionUtils; import org.openhab.binding.hue.internal.Config; import org.openhab.binding.hue.internal.ConfigUpdate; @@ -96,6 +98,7 @@ private static enum StatusType { } private final Logger logger = LoggerFactory.getLogger(HueBridgeHandler.class); + private final @NonNullByDefault({}) HueStateDescriptionOptionProvider stateDescriptionOptionProvider; private final Map lastLightStates = new ConcurrentHashMap<>(); private final Map lastSensorStates = new ConcurrentHashMap<>(); @@ -145,8 +148,6 @@ public void run() { } } - protected abstract void doConnectedRun() throws IOException, ApiException; - private boolean isReachable(String ipAddress) { try { // note that InetAddress.isReachable is unreliable, see @@ -169,6 +170,8 @@ private boolean isReachable(String ipAddress) { } return true; } + + protected abstract void doConnectedRun() throws IOException, ApiException; } private final Runnable sensorPollingRunnable = new PollingRunnable() { @@ -205,6 +208,11 @@ protected void doConnectedRun() throws IOException, ApiException { private final Runnable lightPollingRunnable = new PollingRunnable() { @Override protected void doConnectedRun() throws IOException, ApiException { + updateLights(); + updateGroups(); + } + + private void updateLights() throws IOException, ApiException { Map lastLightStateCopy = new HashMap<>(lastLightStates); List lights; @@ -214,19 +222,19 @@ protected void doConnectedRun() throws IOException, ApiException { lights = hueBridge.getFullConfig().getLights(); } - for (final FullLight fullLight : lights) { - final String lightId = fullLight.getId(); - lastLightStates.put(lightId, fullLight); + for (final FullLight uptodateFullLight : lights) { + final String lightId = uptodateFullLight.getId(); + lastLightStates.put(lightId, uptodateFullLight); if (lastLightStateCopy.containsKey(lightId)) { - final FullLight lastFullLight = lastLightStateCopy.remove(lightId); - final State lastFullLightState = lastFullLight.getState(); - if (!lastFullLightState.equals(fullLight.getState())) { + final FullLight oldFullLight = lastLightStateCopy.remove(lightId); + final State oldFullLightState = oldFullLight.getState(); + if (!oldFullLightState.equals(uptodateFullLight.getState())) { logger.debug("Status update for Hue light '{}' detected.", lightId); - notifyLightStatusListeners(fullLight, StatusType.CHANGED); + notifyLightStatusListeners(uptodateFullLight, StatusType.CHANGED); } } else { logger.debug("Hue light '{}' added.", lightId); - notifyLightStatusListeners(fullLight, StatusType.ADDED); + notifyLightStatusListeners(uptodateFullLight, StatusType.ADDED); } } @@ -236,7 +244,9 @@ protected void doConnectedRun() throws IOException, ApiException { logger.debug("Hue light '{}' removed.", fullLightEntry.getKey()); notifyLightStatusListeners(fullLightEntry.getValue(), StatusType.REMOVED); } + } + private void updateGroups() throws IOException, ApiException { Map lastGroupStateCopy = new HashMap<>(lastGroupStates); for (final FullGroup fullGroup : hueBridge.getGroups()) { @@ -247,8 +257,8 @@ protected void doConnectedRun() throws IOException, ApiException { State colorRef = null; HSBType firstColorHsb = null; for (String lightId : fullGroup.getLights()) { - FullLight light = lastLightStates.get(lightId); - if (light != null) { + if (lastLightStates.containsKey(lightId)) { + FullLight light = lastLightStates.get(lightId); final State lightState = light.getState(); logger.trace("Group {}: light {}: on {} bri {} hue {} sat {} temp {} mode {} XY {}", fullGroup.getName(), light.getName(), lightState.isOn(), lightState.getBrightness(), @@ -309,8 +319,22 @@ protected void doConnectedRun() throws IOException, ApiException { } }; + private final Runnable scenePollingRunnable = new PollingRunnable() { + @Override + protected void doConnectedRun() throws IOException, ApiException { + Map groupNames = lastGroupStates.entrySet().parallelStream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getName())); + List stateOptions = hueBridge.getScenes().parallelStream()// + .map(scene -> scene.toStateOption(groupNames))// + .collect(Collectors.toList()); + stateDescriptionOptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SCENE), + stateOptions); + } + }; + private long lightPollingInterval = TimeUnit.SECONDS.toSeconds(10); private long sensorPollingInterval = TimeUnit.MILLISECONDS.toMillis(500); + private long scenePollingInterval = TimeUnit.MILLISECONDS.convert(10, TimeUnit.MINUTES); private boolean lastBridgeConnectionState = false; @@ -318,12 +342,14 @@ protected void doConnectedRun() throws IOException, ApiException { private @Nullable ScheduledFuture lightPollingJob; private @Nullable ScheduledFuture sensorPollingJob; + private @Nullable ScheduledFuture scenePollingJob; private @NonNullByDefault({}) HueBridge hueBridge = null; private @NonNullByDefault({}) HueBridgeConfig hueBridgeConfig = null; - public HueBridgeHandler(Bridge bridge) { + public HueBridgeHandler(Bridge bridge, HueStateDescriptionOptionProvider stateDescriptionOptionProvider) { super(bridge); + this.stateDescriptionOptionProvider = stateDescriptionOptionProvider; } @Override @@ -482,11 +508,26 @@ private void stopSensorPolling() { } } + private void startScenePolling() { + if (scenePollingJob == null || scenePollingJob.isCancelled()) { + scenePollingJob = scheduler.scheduleWithFixedDelay(scenePollingRunnable, 5000, scenePollingInterval, + TimeUnit.MILLISECONDS); + } + } + + private void stopScenePolling() { + if (scenePollingJob != null && !scenePollingJob.isCancelled()) { + scenePollingJob.cancel(true); + scenePollingJob = null; + } + } + @Override public void dispose() { logger.debug("Handler disposed."); stopLightPolling(); stopSensorPolling(); + stopScenePolling(); if (hueBridge != null) { hueBridge = null; } @@ -514,6 +555,7 @@ private synchronized void onUpdate() { if (hueBridge != null) { startLightPolling(); startSensorPolling(); + startScenePolling(); } } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueStateDescriptionOptionProvider.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueStateDescriptionOptionProvider.java new file mode 100644 index 0000000000000..51018cd4e9324 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueStateDescriptionOptionProvider.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.smarthome.core.thing.binding.BaseDynamicStateDescriptionProvider; +import org.eclipse.smarthome.core.thing.i18n.ChannelTypeI18nLocalizationService; +import org.eclipse.smarthome.core.thing.type.DynamicStateDescriptionProvider; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * Dynamic provider of state options for {@link HueBridgeHandler} while leaving other state description fields as + * original. + * + * @author Hengrui Jiang - Initial contribution + */ +@Component(service = { DynamicStateDescriptionProvider.class, HueStateDescriptionOptionProvider.class }) +@NonNullByDefault +public class HueStateDescriptionOptionProvider extends BaseDynamicStateDescriptionProvider { + + @Reference + protected void setChannelTypeI18nLocalizationService( + final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { + this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService; + } + + protected void unsetChannelTypeI18nLocalizationService( + final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) { + this.channelTypeI18nLocalizationService = null; + } +}