Skip to content

Commit

Permalink
Dynamic state options for scene channel of the bridge
Browse files Browse the repository at this point in the history
  • Loading branch information
leluna committed May 5, 2020
1 parent f4060b9 commit 5a1abf2
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<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().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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand All @@ -69,6 +71,9 @@ public class HueThingHandlerFactory extends BaseThingHandlerFactory {

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

@NonNullByDefault({})
private HueStateDescriptionOptionProvider stateOptionProvider;

@Override
public @Nullable Thing createThing(ThingTypeUID thingTypeUID, Configuration configuration,
@Nullable ThingUID thingUID, @Nullable ThingUID bridgeUID) {
Expand Down Expand Up @@ -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())) {
Expand Down Expand Up @@ -182,4 +187,13 @@ protected synchronized void removeHandler(ThingHandler thingHandler) {
}
}
}

@Reference
protected void setSceneChannelTypeProvider(HueStateDescriptionOptionProvider stateOptionProvider) {
this.stateOptionProvider = stateOptionProvider;
}

protected void unsetSceneChannelTypeProvider(HueStateDescriptionOptionProvider stateOptionProvider) {
this.stateOptionProvider = null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* 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.
*/
public class Scene {
public static final Type GSON_TYPE = new TypeToken<Map<String, Scene>>() {
}.getType();

private String id;
private String name;
@SerializedName("lights")
private List<String> 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<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 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<String, String> groupNames) {
if (id.contentEquals(name)) {
return new StateOption(id, id);
} else {
StringBuilder stateOptionLabel = new StringBuilder(name);
if (groupId != null && groupNames.containsKey(groupId)) {
stateOptionLabel.append(" ").append(groupNames.get(groupId));
}
stateOptionLabel.append(" ( ").append(id).append(")");
return new StateOption(id, stateOptionLabel.toString());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
Expand All @@ -44,6 +45,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;
Expand Down Expand Up @@ -95,6 +97,7 @@ private static enum StatusType {
}

private final Logger logger = LoggerFactory.getLogger(HueBridgeHandler.class);
private final @NonNullByDefault({}) HueStateDescriptionOptionProvider stateDescriptionOptionProvider;

private final Map<String, FullLight> lastLightStates = new ConcurrentHashMap<>();
private final Map<String, FullSensor> lastSensorStates = new ConcurrentHashMap<>();
Expand Down Expand Up @@ -144,8 +147,6 @@ public void run() {
}
}

protected abstract void doConnectedRun() throws IOException, ApiException;

private boolean isReachable(String ipAddress) {
try {
// note that InetAddress.isReachable is unreliable, see
Expand All @@ -168,6 +169,8 @@ private boolean isReachable(String ipAddress) {
}
return true;
}

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

private final Runnable sensorPollingRunnable = new PollingRunnable() {
Expand Down Expand Up @@ -204,6 +207,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<String, FullLight> lastLightStateCopy = new HashMap<>(lastLightStates);

List<FullLight> lights;
Expand All @@ -213,19 +221,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);
}
}

Expand All @@ -235,7 +243,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<String, FullGroup> lastGroupStateCopy = new HashMap<>(lastGroupStates);

for (final FullGroup fullGroup : hueBridge.getGroups()) {
Expand All @@ -246,8 +256,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(),
Expand Down Expand Up @@ -308,21 +318,37 @@ protected void doConnectedRun() throws IOException, ApiException {
}
};

private final Runnable scenePollingRunnable = new PollingRunnable() {
@Override
protected void doConnectedRun() throws IOException, ApiException {
Map<String, String> groupNames = lastGroupStates.entrySet().parallelStream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getName()));
List<StateOption> 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;

private boolean propertiesInitializedSuccessfully = false;

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
Expand Down Expand Up @@ -509,11 +535,26 @@ private void stopSensorPolling() {
}
}

private void startScenePolling() {
if (scenePollingJob == null || scenePollingJob.isCancelled()) {
scenePollingJob = scheduler.scheduleWithFixedDelay(scenePollingRunnable, 1, 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;
}
Expand Down Expand Up @@ -541,6 +582,7 @@ private synchronized void onUpdate() {
if (hueBridge != null) {
startLightPolling();
startSensorPolling();
startScenePolling();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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.
*/
@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;
}
}

0 comments on commit 5a1abf2

Please sign in to comment.