import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.hue.internal.dto.clip2.enums.RecallAction;
+import org.openhab.binding.hue.internal.dto.clip2.enums.SceneRecallAction;
+import org.openhab.binding.hue.internal.dto.clip2.enums.SmartSceneRecallAction;
/**
- * DTO for scene recall.
+ * DTO for scene and smart scene recall.
*
* @author Andrew Fiddian-Green - Initial contribution
*/
private @Nullable @SuppressWarnings("unused") String status;
private @Nullable @SuppressWarnings("unused") Long duration;
- public Recall setAction(RecallAction action) {
+ public Recall setAction(SceneRecallAction action) {
+ this.action = action.name().toLowerCase();
+ return this;
+ }
+
+ public Recall setAction(SmartSceneRecallAction action) {
this.action = action.name().toLowerCase();
return this;
}
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.hue.internal.dto.clip2.enums.ActionType;
import org.openhab.binding.hue.internal.dto.clip2.enums.EffectType;
-import org.openhab.binding.hue.internal.dto.clip2.enums.RecallAction;
import org.openhab.binding.hue.internal.dto.clip2.enums.ResourceType;
+import org.openhab.binding.hue.internal.dto.clip2.enums.SceneRecallAction;
+import org.openhab.binding.hue.internal.dto.clip2.enums.SmartSceneRecallAction;
+import org.openhab.binding.hue.internal.dto.clip2.enums.SmartSceneState;
import org.openhab.binding.hue.internal.dto.clip2.enums.ZigbeeStatus;
import org.openhab.binding.hue.internal.exceptions.DTOPresentButEmptyException;
import org.openhab.core.library.types.DecimalType;
private @Nullable List<ResourceReference> children;
private @Nullable JsonElement status;
private @Nullable @SuppressWarnings("unused") Dynamics dynamics;
+ private @Nullable String state;
/**
* Constructor
* @return either 'UnDefType.NULL', a StringType containing the (active) scene name, or 'UnDefType.UNDEF'.
*/
public State getSceneState() {
- Optional<Boolean> active = getSceneActive();
- return active.isEmpty() ? UnDefType.NULL : active.get() ? new StringType(getName()) : UnDefType.UNDEF;
+ return getSceneActive().map(a -> a ? new StringType(getName()) : UnDefType.UNDEF).orElse(UnDefType.NULL);
+ }
+
+ /**
+ * Check if the smart scene resource contains a 'state' element. If such an element is present, returns a Boolean
+ * Optional whose value depends on the value of that element, or an empty Optional if it is not.
+ *
+ * @return true, false, or empty.
+ */
+ public Optional<Boolean> getSmartSceneActive() {
+ if (ResourceType.SMART_SCENE == getType()) {
+ String state = this.state;
+ if (Objects.nonNull(state)) {
+ return Optional.of(SmartSceneState.ACTIVE == SmartSceneState.of(state));
+ }
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * If the getSmartSceneActive() optional result is empty return 'UnDefType.NULL'. Otherwise if the optional result
+ * is present and 'true' (i.e. the scene is active) return the smart scene name. Or finally (the optional result is
+ * present and 'false') return 'UnDefType.UNDEF'.
+ *
+ * @return either 'UnDefType.NULL', a StringType containing the (active) scene name, or 'UnDefType.UNDEF'.
+ */
+ public State getSmartSceneState() {
+ return getSmartSceneActive().map(a -> a ? new StringType(getName()) : UnDefType.UNDEF).orElse(UnDefType.NULL);
}
public List<ResourceReference> getServiceReferences() {
this.on = on;
}
- public Resource setRecallAction(RecallAction recallAction) {
+ public Resource setRecallAction(SceneRecallAction recallAction) {
+ Recall recall = this.recall;
+ this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setAction(recallAction);
+ return this;
+ }
+
+ public Resource setRecallAction(SmartSceneRecallAction recallAction) {
Recall recall = this.recall;
this.recall = ((Objects.nonNull(recall) ? recall : new Recall())).setAction(recallAction);
return this;
+++ /dev/null
-/**
- * Copyright (c) 2010-2023 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.dto.clip2.enums;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-
-/**
- * Enum for scene recall actions.
- *
- * @author Andrew Fiddian-Green - Initial contribution
- */
-@NonNullByDefault
-public enum RecallAction {
- ACTIVE,
- DYNAMIC_PALETTE,
- STATIC;
-
- public static RecallAction of(@Nullable String value) {
- if (value != null) {
- try {
- return valueOf(value.toUpperCase());
- } catch (IllegalArgumentException e) {
- // fall through
- }
- }
- return ACTIVE;
- }
-}
ROOM,
RELATIVE_ROTARY,
SCENE,
+ SMART_SCENE,
TEMPERATURE,
ZGP_CONNECTIVITY,
ZIGBEE_CONNECTIVITY,
--- /dev/null
+/**
+ * Copyright (c) 2010-2023 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.dto.clip2.enums;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Enum for scene recall actions.
+ *
+ * @author Andrew Fiddian-Green - Initial contribution
+ */
+@NonNullByDefault
+public enum SceneRecallAction {
+ ACTIVE,
+ DYNAMIC_PALETTE,
+ STATIC,
+ UNKNOWN;
+
+ public static SceneRecallAction of(@Nullable String value) {
+ if (value != null) {
+ try {
+ return valueOf(value.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ // fall through
+ }
+ }
+ return UNKNOWN;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2023 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.dto.clip2.enums;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Enum for smart scene recall actions.
+ *
+ * @author Andrew Fiddian-Green - Initial contribution
+ */
+@NonNullByDefault
+public enum SmartSceneRecallAction {
+ ACTIVATE,
+ DEACTIVATE,
+ UNKNOWN;
+
+ public static SmartSceneRecallAction of(@Nullable String value) {
+ if (value != null) {
+ try {
+ return valueOf(value.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ // fall through
+ }
+ }
+ return UNKNOWN;
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2023 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.dto.clip2.enums;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Enum for 'smart_scene' states.
+ *
+ * @author Andrew Fiddian-Green - Initial contribution
+ */
+@NonNullByDefault
+public enum SmartSceneState {
+ INACTIVE,
+ ACTIVE,
+ UNKNOWN;
+
+ public static SmartSceneState of(@Nullable String value) {
+ if (value != null) {
+ try {
+ return valueOf(value.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ // fall through
+ }
+ }
+ return UNKNOWN;
+ }
+}
private static final ResourceReference BRIDGE = new ResourceReference().setType(ResourceType.BRIDGE);
private static final ResourceReference BRIDGE_HOME = new ResourceReference().setType(ResourceType.BRIDGE_HOME);
private static final ResourceReference SCENE = new ResourceReference().setType(ResourceType.SCENE);
+ private static final ResourceReference SMART_SCENE = new ResourceReference().setType(ResourceType.SMART_SCENE);
/**
* List of resource references that need to be mass down loaded.
for (ResourceReference reference : MASS_DOWNLOAD_RESOURCE_REFERENCES) {
ResourceType resourceType = reference.getType();
List<Resource> resourceList = bridge.getResources(reference).getResources();
- if (resourceType == ResourceType.ZONE) {
- // add special 'All Lights' zone to the zone resource list
- resourceList.addAll(bridge.getResources(BRIDGE_HOME).getResources());
+ switch (resourceType) {
+ case ZONE:
+ // add special 'All Lights' zone to the zone resource list
+ resourceList.addAll(bridge.getResources(BRIDGE_HOME).getResources());
+ break;
+
+ case SCENE:
+ // add 'smart scenes' to the scene resource list
+ resourceList.addAll(bridge.getResources(SMART_SCENE).getResources());
+ break;
+
+ default:
+ break;
}
getThing().getThings().forEach(thing -> {
ThingHandler handler = thing.getHandler();
import java.math.BigDecimal;
import java.time.Duration;
import java.time.Instant;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import org.openhab.binding.hue.internal.dto.clip2.TimedEffects;
import org.openhab.binding.hue.internal.dto.clip2.enums.ActionType;
import org.openhab.binding.hue.internal.dto.clip2.enums.EffectType;
-import org.openhab.binding.hue.internal.dto.clip2.enums.RecallAction;
import org.openhab.binding.hue.internal.dto.clip2.enums.ResourceType;
+import org.openhab.binding.hue.internal.dto.clip2.enums.SceneRecallAction;
+import org.openhab.binding.hue.internal.dto.clip2.enums.SmartSceneRecallAction;
import org.openhab.binding.hue.internal.dto.clip2.enums.ZigbeeStatus;
import org.openhab.binding.hue.internal.dto.clip2.helper.Setters;
import org.openhab.binding.hue.internal.exceptions.ApiException;
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_DEVICE, THING_TYPE_ROOM,
THING_TYPE_ZONE);
+ private static final Set<ResourceType> SUPPORTED_SCENE_TYPES = Set.of(ResourceType.SCENE, ResourceType.SMART_SCENE);
+
private static final Duration DYNAMICS_ACTIVE_WINDOW = Duration.ofSeconds(10);
private static final String LK_WISER_DIMMER_MODEL_ID = "LK Dimmer";
private final Set<String> supportedChannelIdSet = new HashSet<>();
/**
- * A map of scene IDs and respective scene Resources for the scenes that contribute to and command this thing. It is
- * a map between the resource ID (string) and a Resource object containing the scene's last known state.
+ * A map of scene IDs versus scene Resources for the scenes that contribute to and command this thing. It is a map
+ * between the resource ID (string) and a Resource object containing the scene's last known state.
*/
private final Map<String, Resource> sceneContributorsCache = new ConcurrentHashMap<>();
/**
- * A map of scene names versus Resource IDs for the scenes that contribute to and command this thing. e.g. a command
- * for a scene named 'Energize' shall be sent to the respective SCENE resource ID.
+ * A map of scene names versus scene Resources for the scenes that contribute to and command this thing. e.g. a
+ * command for a scene named 'Energize' shall be sent to the respective SCENE resource ID.
*/
- private final Map<String, String> sceneResourceIds = new ConcurrentHashMap<>();
+ private final Map<String, Resource> sceneResourceEntries = new ConcurrentHashMap<>();
/**
* A list of API v1 thing channel UIDs that are linked to items. It is used in the process of replicating the
updateServiceContributorsTask = null;
legacyLinkedChannelUIDs.clear();
sceneContributorsCache.clear();
- sceneResourceIds.clear();
+ sceneResourceEntries.clear();
supportedChannelIdSet.clear();
commandResourceIds.clear();
serviceContributorsCache.clear();
case CHANNEL_2_SCENE:
if (command instanceof StringType) {
- putResourceId = sceneResourceIds.get(((StringType) command).toString());
- if (Objects.nonNull(putResourceId)) {
- putResource = new Resource(ResourceType.SCENE).setRecallAction(RecallAction.ACTIVE);
+ Resource scene = sceneResourceEntries.get(((StringType) command).toString());
+ if (Objects.nonNull(scene)) {
+ ResourceType putResourceType = scene.getType();
+ putResource = new Resource(putResourceType);
+ switch (putResourceType) {
+ case SCENE:
+ putResource.setRecallAction(SceneRecallAction.ACTIVE);
+ break;
+ case SMART_SCENE:
+ putResource.setRecallAction(SmartSceneRecallAction.ACTIVATE);
+ break;
+ default:
+ logger.debug("{} -> handleCommand() type '{}' is not a supported scene type",
+ resourceId, putResourceType);
+ return;
+ }
+ putResourceId = scene.getId();
}
}
break;
cancelTask(updateDependenciesTask, false);
updateDependenciesTask = scheduler.submit(() -> updateDependencies());
}
- } else if (ResourceType.SCENE == resource.getType()) {
+ } else if (SUPPORTED_SCENE_TYPES.contains(resource.getType())) {
Resource cachedScene = sceneContributorsCache.get(incomingResourceId);
if (Objects.nonNull(cachedScene)) {
Setters.setResource(resource, cachedScene);
updateState(CHANNEL_2_SCENE, resource.getSceneState(), fullUpdate);
break;
+ case SMART_SCENE:
+ updateState(CHANNEL_2_SCENE, resource.getSmartSceneState(), fullUpdate);
+ break;
+
default:
return false;
}
}
/**
- * Fetch the full list of scenes from the bridge, and call {@code updateSceneContributors(List<Resource> allScenes)}
+ * Fetch the full list of normal resp. smart scenes from the bridge, and call
+ * {@code updateSceneContributors(List<Resource> allScenes)}
*
* @throws ApiException if a communication error occurred.
* @throws AssetNotLoadedException if one of the assets is not loaded.
*/
public boolean updateSceneContributors() throws ApiException, AssetNotLoadedException, InterruptedException {
if (!disposing && !updateSceneContributorsDone) {
- ResourceReference scenesReference = new ResourceReference().setType(ResourceType.SCENE);
- updateSceneContributors(getBridgeHandler().getResources(scenesReference).getResources());
+ List<Resource> allScenes = new ArrayList<>();
+ for (ResourceType type : SUPPORTED_SCENE_TYPES) {
+ allScenes.addAll(getBridgeHandler().getResources(new ResourceReference().setType(type)).getResources());
+ }
+ updateSceneContributors(allScenes);
}
return updateSceneContributorsDone;
}
/**
- * Process the incoming list of scene resources to find those scenes which contribute to this thing. And if there
- * are any, include a scene channel in the supported channel list, and populate its respective state options.
+ * Process the incoming list of normal resp. smart scene resources to find those which contribute to this thing. And
+ * if there are any, include a scene channel in the supported channel list, and populate its respective state
+ * options.
*
- * @param allScenes the full list of scene resources.
+ * @param allScenes the full list of normal resp. smart scene resources.
*/
public synchronized boolean updateSceneContributors(List<Resource> allScenes) {
if (!disposing && !updateSceneContributorsDone) {
sceneContributorsCache.clear();
- sceneResourceIds.clear();
+ sceneResourceEntries.clear();
ResourceReference thisReference = getResourceReference();
List<Resource> scenes = allScenes.stream().filter(s -> thisReference.equals(s.getGroup()))
if (!scenes.isEmpty()) {
sceneContributorsCache.putAll(scenes.stream().collect(Collectors.toMap(s -> s.getId(), s -> s)));
- sceneResourceIds.putAll(scenes.stream().collect(Collectors.toMap(s -> s.getName(), s -> s.getId())));
+ sceneResourceEntries.putAll(scenes.stream().collect(Collectors.toMap(s -> s.getName(), s -> s)));
State state = scenes.stream().filter(s -> s.getSceneActive().orElse(false)).map(s -> s.getSceneState())
.findAny().orElse(UnDefType.UNDEF);
+
updateState(CHANNEL_2_SCENE, state, true);
stateDescriptionProvider.setStateOptions(new ChannelUID(thing.getUID(), CHANNEL_2_SCENE), scenes
.stream().map(s -> s.getName()).map(n -> new StateOption(n, n)).collect(Collectors.toList()));
- logger.debug("{} -> updateSceneContributors() found {} scenes", resourceId, scenes.size());
+ logger.debug("{} -> updateSceneContributors() found {} normal resp. smart scenes", resourceId,
+ scenes.size());
}
updateSceneContributorsDone = true;
}
assertEquals(OnOffType.ON, action.getOnOffState());
}
+ @Test
+ void testSmartScene() {
+ String json = load(ResourceType.SMART_SCENE.name().toLowerCase());
+ Resources resources = GSON.fromJson(json, Resources.class);
+ assertNotNull(resources);
+ List<Resource> list = resources.getResources();
+ assertNotNull(list);
+ assertEquals(1, list.size());
+ Resource item = list.get(0);
+ ResourceReference group = item.getGroup();
+ assertNotNull(group);
+ String groupId = group.getId();
+ assertNotNull(groupId);
+ assertFalse(groupId.isBlank());
+ ResourceType type = group.getType();
+ assertNotNull(type);
+ assertEquals(ResourceType.ROOM, type);
+ Optional<Boolean> state = item.getSmartSceneActive();
+ assertTrue(state.isPresent());
+ assertFalse(state.get());
+ }
+
@Test
void testSensor2Motion() {
String json = load(ResourceType.MOTION.name().toLowerCase());
--- /dev/null
+{
+ "errors": [
+ ],
+ "data": [
+ {
+ "id": "0707ec71-8d7a-4bf8-91ca-71db273ddfe9",
+ "type": "smart_scene",
+ "metadata": {
+ "name": "Natural light",
+ "image": {
+ "rid": "eb014820-a902-4652-8ca7-6e29c03b87a1",
+ "rtype": "public_image"
+ }
+ },
+ "group": {
+ "rid": "1166743f-fe3d-47d1-bd7d-b5bb378098cc",
+ "rtype": "room"
+ },
+ "week_timeslots": [
+ {
+ "timeslots": [
+ {
+ "start_time": {
+ "kind": "time",
+ "time": {
+ "hour": 7,
+ "minute": 0,
+ "second": 0
+ }
+ },
+ "target": {
+ "rid": "9c633a2d-f7bf-4bba-b5ee-a4d69ce6e050",
+ "rtype": "scene"
+ }
+ },
+ {
+ "start_time": {
+ "kind": "time",
+ "time": {
+ "hour": 10,
+ "minute": 0,
+ "second": 0
+ }
+ },
+ "target": {
+ "rid": "7616d9fe-4889-472b-93bf-5090c19fb902",
+ "rtype": "scene"
+ }
+ },
+ {
+ "start_time": {
+ "kind": "sunset",
+ "time": {
+ "hour": 0,
+ "minute": 0,
+ "second": 0
+ }
+ },
+ "target": {
+ "rid": "eeabcfdb-97db-4e4a-a94a-fe1fea8e6c2c",
+ "rtype": "scene"
+ }
+ },
+ {
+ "start_time": {
+ "kind": "time",
+ "time": {
+ "hour": 20,
+ "minute": 0,
+ "second": 0
+ }
+ },
+ "target": {
+ "rid": "2f474323-0c6b-4361-a8c7-b52da902cd7b",
+ "rtype": "scene"
+ }
+ },
+ {
+ "start_time": {
+ "kind": "time",
+ "time": {
+ "hour": 22,
+ "minute": 0,
+ "second": 0
+ }
+ },
+ "target": {
+ "rid": "6fa2a278-43ec-4586-bdd8-2fe6f4ea5f85",
+ "rtype": "scene"
+ }
+ },
+ {
+ "start_time": {
+ "kind": "time",
+ "time": {
+ "hour": 0,
+ "minute": 0,
+ "second": 0
+ }
+ },
+ "target": {
+ "rid": "6ca12be2-c547-4722-8db0-6e12f20688f3",
+ "rtype": "scene"
+ }
+ }
+ ],
+ "recurrence": [
+ "sunday",
+ "monday",
+ "tuesday",
+ "wednesday",
+ "thursday",
+ "friday",
+ "saturday"
+ ]
+ }
+ ],
+ "transition_duration": 60000,
+ "active_timeslot": {
+ "timeslot_id": 1,
+ "weekday": "wednesday"
+ },
+ "state": "inactive"
+ }
+ ]
+}