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: Hengrui Jiang <[email protected]>
  • Loading branch information
leluna committed Sep 8, 2019
1 parent 679948d commit 22c621f
Show file tree
Hide file tree
Showing 13 changed files with 235 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
* @author Samuel Leisering - added API-Version
*/
public class Config {
private String bridgeid;
private String name;
private String swversion;
private String apiversion;
Expand All @@ -43,6 +44,15 @@ public class Config {
Config() {
}

/**
* Returns the bridge ID.
*
* @return unique ID of the bridge
*/
public String getBridgeId() {
return bridgeid;
}

/**
* Returns the name.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public class HueBindingConstants {

// bridge
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge");
public static final ThingTypeUID THING_TYPE_GATEWAY = new ThingTypeUID(BINDING_ID, "gateway");

// generic thing types
public static final ThingTypeUID THING_TYPE_ON_OFF_LIGHT = new ThingTypeUID(BINDING_ID, "0000");
Expand Down Expand Up @@ -67,6 +68,7 @@ public class HueBindingConstants {
public static final String CHANNEL_LIGHT_LEVEL = "light_level";
public static final String CHANNEL_DARK = "dark";
public static final String CHANNEL_DAYLIGHT = "daylight";
public static final String CHANNEL_SCENE = "scene";

// List all triggers
public static final String EVENT_DIMMER_SWITCH = "dimmer_switch_event";
Expand All @@ -78,12 +80,15 @@ 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";
public static final String UNIQUE_ID = "uniqueId";
public static final String FADETIME = "fadetime";

// Gateway config properties
public static final String BRIDGE_ID = "bridgeId";

public static final String NORMALIZE_ID_REGEX = "[^a-zA-Z0-9_]";
}
Original file line number Diff line number Diff line change
Expand Up @@ -583,13 +583,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 @@ -830,6 +829,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 @@ -35,6 +35,7 @@
import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory;
import org.openhab.binding.hue.internal.discovery.HueLightDiscoveryService;
import org.openhab.binding.hue.internal.handler.HueBridgeHandler;
import org.openhab.binding.hue.internal.handler.HueGatewayHandler;
import org.openhab.binding.hue.internal.handler.HueLightHandler;
import org.openhab.binding.hue.internal.handler.sensors.DimmerSwitchHandler;
import org.openhab.binding.hue.internal.handler.sensors.LightLevelHandler;
Expand All @@ -56,11 +57,12 @@
@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()).flatMap(i -> i).collect(Collectors.toSet()));
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.unmodifiableSet(Stream
.of(HueBridgeHandler.SUPPORTED_THING_TYPES.stream(), HueGatewayHandler.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())
.flatMap(i -> i).collect(Collectors.toSet()));

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

Expand All @@ -69,6 +71,10 @@ public class HueThingHandlerFactory extends BaseThingHandlerFactory {
@Nullable ThingUID thingUID, @Nullable ThingUID bridgeUID) {
if (HueBridgeHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
return super.createThing(thingTypeUID, configuration, thingUID, null);
} else if (HueGatewayHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
ThingUID uid = thingUID != null ? thingUID
: getThingUID(thingTypeUID, configuration.get(BRIDGE_ID).toString(), bridgeUID);
return super.createThing(thingTypeUID, configuration, uid, bridgeUID);
} else if (HueLightHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) {
ThingUID hueLightUID = getLightUID(thingTypeUID, thingUID, configuration, bridgeUID);
return super.createThing(thingTypeUID, configuration, hueLightUID, bridgeUID);
Expand Down Expand Up @@ -121,6 +127,8 @@ private ThingUID getThingUID(ThingTypeUID thingTypeUID, String id, @Nullable Thi
HueBridgeHandler handler = new HueBridgeHandler((Bridge) thing);
registerLightDiscoveryService(handler);
return handler;
} else if (HueGatewayHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) {
return new HueGatewayHandler(thing);
} else if (HueLightHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) {
return new HueLightHandler(thing);
} else if (DimmerSwitchHandler.SUPPORTED_THING_TYPES.contains(thing.getThingTypeUID())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,9 @@ public StateUpdate setTransitionTime(long timeMillis) {
return this;
}

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 @@ -25,6 +25,7 @@
import org.eclipse.smarthome.config.discovery.DiscoveryResult;
import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder;
import org.eclipse.smarthome.config.discovery.upnp.UpnpDiscoveryParticipant;
import org.eclipse.smarthome.config.discovery.upnp.internal.UpnpDiscoveryService;
import org.eclipse.smarthome.core.thing.ThingTypeUID;
import org.eclipse.smarthome.core.thing.ThingUID;
import org.jupnp.model.meta.DeviceDetails;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,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 Expand Up @@ -127,6 +127,24 @@ public void startScan() {
}
// search for unpaired lights
hueBridgeHandler.startSearch();

addBridgeAsGateway();
}

private void addBridgeAsGateway() {
ThingUID bridgeUID = hueBridgeHandler.getThing().getUID();
String bridgeId = hueBridgeHandler.getBridgeId();
ThingUID gatewayUID = new ThingUID(THING_TYPE_GATEWAY, bridgeUID, bridgeId);
Map<String, Object> properties = new HashMap<>();
properties.put(BRIDGE_ID, bridgeId);

DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(gatewayUID)//
.withThingType(THING_TYPE_GATEWAY)//
.withProperties(properties)//
.withBridge(bridgeUID)//
.withRepresentationProperty(BRIDGE_ID)//
.withLabel(hueBridgeHandler.getThing().getLabel()).build();
thingDiscovered(discoveryResult);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,34 +124,35 @@ 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 static final String STATE_ADDED = "added";
private static final String STATE_GONE = "gone";
private static final String STATE_CHANGED = "changed";
private static final String STATE_REMOVED = "removed";

public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE);

Expand Down Expand Up @@ -194,21 +195,14 @@ protected void doConnectedRun() throws IOException, ApiException {
lastSensorStates.put(sensorId, sensor);
logger.debug("Hue sensor '{}' added.", sensorId);
notifySensorStatusListeners(sensor, STATE_ADDED);

}
}

// Check for removed sensors
for (Entry<String, FullSensor> fullSensorEntry : lastSensorStateCopy.entrySet()) {
lastSensorStates.remove(fullSensorEntry.getKey());
logger.debug("Hue sensor '{}' removed.", fullSensorEntry.getKey());
for (SensorStatusListener sensorStatusListener : sensorStatusListeners) {
try {
sensorStatusListener.onSensorRemoved(hueBridge, fullSensorEntry.getValue());
} catch (Exception e) {
logger.error("An exception occurred while calling the Sensor Listeners", e);
}
}
notifySensorStatusListeners(fullSensorEntry.getValue(), STATE_REMOVED);
}
}
};
Expand Down Expand Up @@ -246,13 +240,7 @@ protected void doConnectedRun() throws IOException, ApiException {
for (Entry<String, FullLight> fullLightEntry : lastLightStateCopy.entrySet()) {
lastLightStates.remove(fullLightEntry.getKey());
logger.debug("Hue light '{}' removed.", fullLightEntry.getKey());
for (LightStatusListener lightStatusListener : lightStatusListeners) {
try {
lightStatusListener.onLightRemoved(hueBridge, fullLightEntry.getValue());
} catch (Exception e) {
logger.error("An exception occurred while calling the BridgeHeartbeatListener", e);
}
}
notifyLightStatusListeners(fullLightEntry.getValue(), STATE_REMOVED);
}
}
};
Expand All @@ -263,7 +251,7 @@ public HueBridgeHandler(Bridge bridge) {

@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// not needed
// no commands
}

@Override
Expand Down Expand Up @@ -609,6 +597,24 @@ public boolean unregisterSensorStatusListener(SensorStatusListener sensorStatusL
return result;
}

@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 @@ -691,6 +697,9 @@ private void notifyLightStatusListeners(final FullLight fullLight, final String
logger.debug("Sending lightStateChanged for light '{}'", fullLight.getId());
lightStatusListener.onLightStateChanged(hueBridge, fullLight);
break;
case STATE_REMOVED:
lightStatusListener.onLightRemoved(hueBridge, fullLight);
break;
default:
throw new IllegalArgumentException(
"Could not notify lightStatusListeners for unknown event type " + type);
Expand Down Expand Up @@ -721,6 +730,9 @@ private void notifySensorStatusListeners(final FullSensor fullSensor, final Stri
logger.debug("Sending sensorStateChanged for sensor '{}'", fullSensor.getId());
sensorStatusListener.onSensorStateChanged(hueBridge, fullSensor);
break;
case STATE_REMOVED:
sensorStatusListener.onSensorRemoved(hueBridge, fullSensor);
break;
default:
throw new IllegalArgumentException(
"Could not notify sensorStatusListeners for unknown event type " + type);
Expand Down Expand Up @@ -766,4 +778,8 @@ public Collection<ConfigStatusMessage> getConfigStatus() {
public long getSensorPollingInterval() {
return sensorPollingInterval;
}

public String getBridgeId() {
return withReAuthentication("get bridge ID", () -> hueBridge.getConfig().getBridgeId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,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 @@ -74,7 +74,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 All @@ -94,4 +94,11 @@ public interface HueClient {
* @param configUpdate the config update
*/
void updateSensorConfig(FullSensor sensor, ConfigUpdate configUpdate);

/**
* Activate scene to all lights that belong to the scene.
*
* @param id the ID of the scene to activate
*/
void activateScene(String id);
}
Loading

0 comments on commit 22c621f

Please sign in to comment.