Skip to content

Commit

Permalink
[hue] Add support for hue scene activation (#8098)
Browse files Browse the repository at this point in the history
* [hue] Add support for hue scene activation

Closes #6043

This is the continuation of the PR #7540

95% of credits go to leluna

Signed-off-by: Laurent Garnier <[email protected]>
Also-by: leluna <[email protected]>
  • Loading branch information
lolodomo authored Jul 13, 2020
1 parent 547bd11 commit fbf0aaf
Show file tree
Hide file tree
Showing 22 changed files with 835 additions and 145 deletions.
14 changes: 12 additions & 2 deletions bundles/org.openhab.binding.hue/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,12 @@ The devices support some of the following channels:
| flag | Switch | This channel save flag state for a CLIP sensor. | 0850 |
| status | Number | This channel save status state for a CLIP sensor. | 0840 |
| last_updated | DateTime | This channel the date and time when the sensor was last updated. | 0820, 0830, 0840, 0850, 0106, 0107, 0302|
| battery_level | Number | This channel shows the battery level. | 0820, 0106, 0107, 0302 |
| battery_low | Switch | This channel indicates whether the battery is low or not. | 0820, 0106, 0107, 0302 |
| battery_level | Number | This channel shows the battery level. | 0820, 0106, 0107, 0302 |
| battery_low | Switch | This channel indicates whether the battery is low or not. | 0820, 0106, 0107, 0302 |
| scene | String | This channel activates the scene with the given ID String. The ID String of each scene is assigned by the Hue bridge. | bridge, group |

To load a hue scene inside a rule for example, the ID of the scene will be required.
You can list all the scene IDs with the following console commands: `hue <bridgeUID> scenes` and `hue <groupThingUID> scenes`.

### Trigger Channels

Expand Down Expand Up @@ -301,6 +305,9 @@ Switch MotionSensorLowBattery { channel="hue:0107:1:motion-sensor:battery_lo
// Temperature Sensor
Number:Temperature TemperatureSensorTemperature { channel="hue:0302:1:temperature-sensor:temperature" }
// Scenes
String LightScene { channel="hue:bridge:1:scene"}
```

Note: The bridge ID is in this example **1** but can be different in each system.
Expand Down Expand Up @@ -336,6 +343,9 @@ sitemap demo label="Main Menu"
Text item=MotionSensorLastUpdate
Text item=MotionSensorBatteryLevel
Switch item=MotionSensorLowBattery
// Light Scenes
Default item=LightScene label="Scene []"
}
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ public class FullGroup extends Group {
private State groupState; // Will not be set by hue API

FullGroup() {
super();
}

/**
* Test constructor
*/
FullGroup(String id, String name, String type, State action, List<String> lights, State state) {
super(id, name, type);
this.action = action;
this.lights = lights;
this.groupState = state;
}

/**
Expand All @@ -51,7 +62,7 @@ public State getAction() {
*
* @return lights in the group
*/
public List<String> getLights() {
public List<String> getLightIds() {
return lights;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ public class Group {
this.type = "LightGroup";
}

/**
* Test constructor
*/
Group(String id, String name, String type) {
this.id = id;
this.name = name;
this.type = type;
}

void setName(String name) {
this.name = name;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public class HueBindingConstants {
public static final String CHANNEL_DAYLIGHT = "daylight";
public static final String CHANNEL_STATUS = "status";
public static final String CHANNEL_FLAG = "flag";
public static final String CHANNEL_SCENE = "scene";

// List all triggers
public static final String EVENT_DIMMER_SWITCH = "dimmer_switch_event";
Expand All @@ -86,7 +87,7 @@ public class HueBindingConstants {
public static final String PROTOCOL = "protocol";
public static final String USER_NAME = "userName";

// Light config properties
// Thing configuration properties
public static final String LIGHT_ID = "lightId";
public static final String SENSOR_ID = "sensorId";
public static final String PRODUCT_NAME = "productName";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
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;
Expand Down Expand Up @@ -114,6 +116,17 @@ public HueBridge(String ip, int port, String protocol, String username, Schedule
authenticate(username);
}

/**
* Test constructor
*/
HueBridge(String ip, String baseUrl, String username, ScheduledExecutorService scheduler, HttpClient http) {
this.ip = ip;
this.baseUrl = baseUrl;
this.username = username;
this.scheduler = scheduler;
this.http = http;
}

/**
* Set the connect and read timeout for HTTP requests.
*
Expand Down Expand Up @@ -851,6 +864,39 @@ 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<Scene> getScenes() throws IOException, ApiException {
requireAuthentication();

Result result = http.get(getRelativeURL("scenes"));
handleErrors(result);

Map<String, Scene> sceneMap = safeFromJson(result.getBody(), Scene.GSON_TYPE);
return sceneMap.entrySet().stream()//
.map(e -> {
e.getValue().setId(e.getKey());
return e.getValue();
})//
.filter(scene -> !scene.isRecycle())//
.sorted(Comparator.comparing(Scene::extractKeyForComparator))//
.collect(Collectors.toList());
}

/**
* Activate scene to all lights that belong to the scene.
*
* @param id the scene to be activated
* @throws IOException if the bridge cannot be reached
*/
public CompletableFuture<Result> recallScene(String id) {
Group allLightsGroup = new Group();
return setGroupState(allLightsGroup, new StateUpdate().setScene(id));
}

/**
* Authenticate on the bridge as the specified user.
* This function verifies that the specified username is valid and will use
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import com.google.gson.reflect.TypeToken;

/**
* Basic light information.
* Basic hue object information.
*
* @author Q42 - Initial contribution
* @author Denis Dudnik - moved Jue library source code inside the smarthome Hue binding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,17 @@
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;
import org.openhab.binding.hue.internal.handler.sensors.PresenceHandler;
import org.openhab.binding.hue.internal.handler.sensors.TapSwitchHandler;
import org.openhab.binding.hue.internal.handler.sensors.TemperatureHandler;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

/**
* {@link HueThingHandlerFactory} is a factory for {@link HueBridgeHandler}s.
Expand All @@ -59,15 +62,23 @@
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.hue")
public class HueThingHandlerFactory extends BaseThingHandlerFactory {

public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.unmodifiableSet(
Stream.of(HueBridgeHandler.SUPPORTED_THING_TYPES.stream(), HueLightHandler.SUPPORTED_THING_TYPES.stream(),
DimmerSwitchHandler.SUPPORTED_THING_TYPES.stream(), TapSwitchHandler.SUPPORTED_THING_TYPES.stream(),
PresenceHandler.SUPPORTED_THING_TYPES.stream(), TemperatureHandler.SUPPORTED_THING_TYPES.stream(),
LightLevelHandler.SUPPORTED_THING_TYPES.stream(), ClipHandler.SUPPORTED_THING_TYPES.stream(),
HueGroupHandler.SUPPORTED_THING_TYPES.stream()).flatMap(i -> i).collect(Collectors.toSet()));

private final HueStateDescriptionOptionProvider stateOptionProvider;

private final Map<ThingUID, @Nullable ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();

@Activate
public HueThingHandlerFactory(final @Reference HueStateDescriptionOptionProvider stateOptionProvider) {
this.stateOptionProvider = stateOptionProvider;
}

@Override
public @Nullable Thing createThing(ThingTypeUID thingTypeUID, Configuration configuration,
@Nullable ThingUID thingUID, @Nullable ThingUID bridgeUID) {
Expand Down Expand Up @@ -135,7 +146,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())) {
Expand All @@ -153,7 +164,7 @@ private ThingUID getThingUID(ThingTypeUID thingTypeUID, String id, @Nullable Thi
} else if (ClipHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) {
return new ClipHandler(thing);
} else if (HueGroupHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) {
return new HueGroupHandler(thing);
return new HueGroupHandler(thing, stateOptionProvider);
} else {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/**
* 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
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
*/
@NonNullByDefault
public class Scene {
public static final Type GSON_TYPE = new TypeToken<Map<String, Scene>>() {
}.getType();

private @NonNullByDefault({}) String id;
private @NonNullByDefault({}) String name;
@SerializedName("lights")
private @NonNullByDefault({}) List<String> lightIds;
@SerializedName("group")
private @Nullable String groupId;
private boolean recycle;

/**
* Default constructor for GSon.
*/
public Scene() {
super();
}

/**
* Test constructor
*/
Scene(String id, String name, @Nullable String groupId, List<String> lightIds, boolean recycle) {
this.id = id;
this.name = name;
this.groupId = groupId;
this.lightIds = lightIds;
this.recycle = recycle;
}

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
*/
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
*/
public List<String> 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 @Nullable 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;
}

/**
* Creates a {@link StateOption} to display this scene, including the group that it belongs to.
* <p>
* The display name is built with the following pattern:
* <ol>
* <li>Human readable name of the scene if set. Otherwise, the ID is displayed</li>
* <li>Group for which the scene is defined</li>
* </ol>
*/
public StateOption toStateOption(Map<String, String> groupNames) {
StringBuilder stateOptionLabel = new StringBuilder(name);
if (groupId != null && groupNames.containsKey(groupId)) {
stateOptionLabel.append(" (").append(groupNames.get(groupId)).append(")");
}

return new StateOption(id, stateOptionLabel.toString());
}

/**
* Creates a {@link StateOption} to display this scene.
*/
public StateOption toStateOption() {
return new StateOption(id, name);
}

/**
* Returns whether the scene is applicable to the given group.
* <p>
* According to the hue API, a scene is applicable to a group if either
* <ol>
* <li>The scene is defined for the group</li>
* <li>All lights of the scene also belong to the group</li>
* </ol>
*/
public boolean isApplicableTo(FullGroup group) {
if (getGroupId() == null) {
return getLightIds().stream().allMatch(id -> group.getLightIds().contains(id));
} else {
return group.getId().contentEquals(getGroupId());
}
}

public String extractKeyForComparator() {
return (groupId != null ? groupId : "") + "#" + name;
}

@Override
public String toString() {
return String.format("{Scene name: %s; id: %s; lightIds: %s; groupId: %s; recycle: %s}", name, id, lightIds,
groupId, recycle);
}
}
Loading

0 comments on commit fbf0aaf

Please sign in to comment.