Skip to content

Commit

Permalink
Add hue gateway as thing to activate predefined scenes
Browse files Browse the repository at this point in the history
Signed-off-by: leluna <[email protected]>
  • Loading branch information
leluna committed May 3, 2020
1 parent 2621ff1 commit 2ba1c72
Show file tree
Hide file tree
Showing 11 changed files with 125 additions and 35 deletions.
19 changes: 18 additions & 1 deletion bundles/org.openhab.binding.hue/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ The devices support some of the following channels:
| 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_low | Switch | This channel indicates whether the battery is low or not. | 0820, 0106, 0107, 0302 |

### Trigger Channels

Expand Down Expand Up @@ -224,6 +224,17 @@ The `tap_switch_event` can trigger one of the following events:
| Button 3 | Button 3 | 17 |
| Button 4 | Button 4 | 18 |

### Scene Channel

The bridge itself supports a channel to activate scenes:

| Channel Type ID | Item Type | Description |
| --------------- | --------- | ----------- |
| scene | String | This channel activates the scene with the given ID String. |

The scenes are identified by an ID String that is assigned by the Hue bridge. These must be aquired directly from [Hue REST API](https://developers.meethue.com/develop/hue-api/).

This channel can then be used in the sitemap, for example as a switch or selection.

## Rule Actions

Expand Down Expand Up @@ -301,6 +312,9 @@ Switch MotionSensorLowBattery { channel="hue:0107:1:motion-sensor:battery_lo
// Temperature Sensor
Number:Temperature TemperatureSensorTemperature { channel="hue:0302: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 +350,9 @@ sitemap demo label="Main Menu"
Text item=MotionSensorLastUpdate
Text item=MotionSensorBatteryLevel
Switch item=MotionSensorLowBattery
// Light Scenes
Switch item=LightScene label="Scene []" mappings=[abcdefgh1234567="Relax", ABCDEFGH1234567="Concentrate"]
}
}
```
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
// Light and sensor config 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 @@ -605,13 +605,12 @@ public String setGroupAttributes(Group group, String name, List<HueObject> light
* @throws UnauthorizedException thrown if the user no longer exists
* @throws EntityNotAvailableException thrown if the specified group no longer exists
*/
public void setGroupState(Group group, StateUpdate update) throws IOException, ApiException {
public CompletableFuture<Result> setGroupState(Group group, StateUpdate update) {
requireAuthentication();

String body = update.toJson();
Result result = http.put(getRelativeURL("groups/" + enc(group.getId()) + "/action"), body);

handleErrors(result);
return http.putAsync(getRelativeURL("groups/" + enc(group.getId()) + "/action"), body, update.getMessageDelay(),
scheduler);
}

/**
Expand Down Expand Up @@ -852,6 +851,17 @@ public void deleteSchedule(Schedule schedule) throws IOException, ApiException {
handleErrors(result);
}

/**
* 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> activateScene(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 @@ -59,6 +59,7 @@
@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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,15 @@ public StateUpdate setStatus(int status) {
commands.add(new Command("status", status));
return this;
}

/**
* Recall the given scene.
*
* @param sceneId Identifier of the scene
* @return this object for chaining calls
*/
public StateUpdate setScene(String sceneId) {
commands.add(new Command("scene", sceneId));
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
import org.slf4j.LoggerFactory;

/**
* The {@link HueBridgeServiceTracker} tracks for hue lights which are connected
* The {@link HueBridgeServiceTracker} tracks for hue lights and sensors which are connected
* to a paired hue bridge. The default search time for hue is 60 seconds.
*
* @author Kai Kreuzer - Initial contribution
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.eclipse.smarthome.config.core.status.ConfigStatusMessage;
import org.eclipse.smarthome.core.library.types.HSBType;
import org.eclipse.smarthome.core.library.types.OnOffType;
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.ThingStatus;
Expand Down Expand Up @@ -144,29 +145,29 @@ public void run() {
}

protected abstract void doConnectedRun() throws IOException, ApiException;
}

private boolean isReachable(String ipAddress) {
try {
// note that InetAddress.isReachable is unreliable, see
// http://stackoverflow.com/questions/9922543/why-does-inetaddress-isreachable-return-false-when-i-can-ping-the-ip-address
// That's why we do an HTTP access instead
private boolean isReachable(String ipAddress) {
try {
// note that InetAddress.isReachable is unreliable, see
// http://stackoverflow.com/questions/9922543/why-does-inetaddress-isreachable-return-false-when-i-can-ping-the-ip-address
// That's why we do an HTTP access instead

// If there is no connection, this line will fail
hueBridge.authenticate("invalid");
} catch (IOException e) {
// If there is no connection, this line will fail
hueBridge.authenticate("invalid");
} catch (IOException e) {
return false;
} catch (ApiException e) {
if (e.getMessage().contains("SocketTimeout") || e.getMessage().contains("ConnectException")
|| e.getMessage().contains("SocketException")
|| e.getMessage().contains("NoRouteToHostException")) {
return false;
} catch (ApiException e) {
if (e.getMessage().contains("SocketTimeout") || e.getMessage().contains("ConnectException")
|| e.getMessage().contains("SocketException")
|| e.getMessage().contains("NoRouteToHostException")) {
return false;
} else {
// this seems to be only an authentication issue
return true;
}
} else {
// this seems to be only an authentication issue
return true;
}
return true;
}
return true;
}

private final Runnable sensorPollingRunnable = new PollingRunnable() {
Expand Down Expand Up @@ -326,7 +327,9 @@ public HueBridgeHandler(Bridge bridge) {

@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// not needed
if (CHANNEL_SCENE.equals(channelUID.getId()) && command instanceof StringType) {
activateScene(command.toString());
}
}

@Override
Expand Down Expand Up @@ -386,11 +389,17 @@ public void updateSensorConfig(FullSensor sensor, ConfigUpdate configUpdate) {
@Override
public void updateGroupState(FullGroup group, StateUpdate stateUpdate) {
if (hueBridge != null) {
try {
hueBridge.setGroupState(group, stateUpdate);
} catch (IOException | ApiException e) {
hueBridge.setGroupState(group, stateUpdate).thenAccept(result -> {
try {
hueBridge.handleErrors(result);
} catch (Exception e) {
handleStateUpdateException(group, stateUpdate, e);
}
}).exceptionally(e -> {
handleStateUpdateException(group, stateUpdate, e);
}
return null;
});

} else {
logger.debug("No bridge connected or selected. Cannot set group state.");
}
Expand Down Expand Up @@ -723,6 +732,29 @@ public boolean unregisterGroupStatusListener(GroupStatusListener groupStatusList
return groupStatusListeners.remove(groupStatusListener);
}

/**
* Activate scene to all lights that belong to the scene.
*
* @param id the ID of the scene to activate
*/
@Override
public void activateScene(String id) {
if (hueBridge != null) {
hueBridge.activateScene(id).thenAccept(result -> {
try {
hueBridge.handleErrors(result);
} catch (Exception e) {
logger.warn("Error while activating scene: {}", e.getMessage(), e);
}
}).exceptionally(e -> {
logger.warn("Error while activating scene: {}", e.getMessage(), e);
return null;
});
} else {
logger.warn("No bridge connected or selected. Cannot activate scene.");
}
}

@Override
public @Nullable FullLight getLightById(String lightId) {
return lastLightStates.get(lightId);
Expand Down Expand Up @@ -773,6 +805,7 @@ public void startSearch(List<String> serialNumbers) {
});
}

@Nullable
private <T> T withReAuthentication(String taskDescription, Callable<T> runnable) {
if (hueBridge != null) {
try {
Expand Down Expand Up @@ -893,7 +926,7 @@ public Collection<ConfigStatusMessage> getConfigStatus() {
Collection<ConfigStatusMessage> configStatusMessages;

// Check whether an IP address is provided
if (hueBridgeConfig.getIpAddress() == null || hueBridgeConfig.getIpAddress().isEmpty()) {
if (hueBridgeConfig.getIpAddress().isEmpty()) {
configStatusMessages = Collections.singletonList(ConfigStatusMessage.Builder.error(HOST)
.withMessageKeySuffix(HueConfigStatusMessage.IP_ADDRESS_MISSING).withArguments(HOST).build());
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public interface HueClient {
* Get the light by its ID.
*
* @param lightId the light ID
* @return the full light representation of {@code null} if it could not be found
* @return the full light representation or {@code null} if it could not be found
*/
@Nullable
FullLight getLightById(String lightId);
Expand All @@ -92,7 +92,7 @@ public interface HueClient {
* Get the sensor by its ID.
*
* @param sensorId the sensor ID
* @return the full sensor representation of {@code null} if it could not be found
* @return the full sensor representation or {@code null} if it could not be found
*/
@Nullable
FullSensor getSensorById(String sensorId);
Expand Down Expand Up @@ -137,4 +137,11 @@ public interface HueClient {
* @param stateUpdate the state update
*/
void updateGroupState(FullGroup group, StateUpdate stateUpdate);

/**
* Activate scene to all lights that belong to the scene.
*
* @param id the ID of the scene to activate
*/
void activateScene(String id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
<label>Hue Bridge</label>
<description>The Hue bridge represents the Philips Hue bridge.</description>

<channels>
<channel id="scene" typeId="scene" />
</channels>

<properties>
<property name="vendor">Philips</property>
</properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,5 +212,11 @@
<label>Flag</label>
<description>Flag of CLIP sensor.</description>
</channel-type>


<!-- Scene Channel -->
<channel-type id="scene">
<item-type>String</item-type>
<label>Scene</label>
<description>The scene channel allows activating a scene to all lights that belong to the scene.</description>
</channel-type>
</thing:thing-descriptions>

0 comments on commit 2ba1c72

Please sign in to comment.