diff --git a/bundles/org.openhab.binding.hue/README.md b/bundles/org.openhab.binding.hue/README.md index a349aa83ba4a5..994f6214cda40 100644 --- a/bundles/org.openhab.binding.hue/README.md +++ b/bundles/org.openhab.binding.hue/README.md @@ -232,9 +232,7 @@ The bridge itself supports a channel to activate scenes: | --------------- | --------- | ----------- | | 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. +The scenes are identified by an ID String that is assigned by the Hue bridge. ## Rule Actions diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/FullGroup.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/FullGroup.java index f51b8974df21e..3492dcbba808f 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/FullGroup.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/FullGroup.java @@ -34,6 +34,17 @@ public class FullGroup extends Group { private State state; // Will not be set by hue API FullGroup() { + super(); + } + + /** + * Test constructor + */ + FullGroup(String id, String name, String type, State action, List lights, State state) { + super(id, name, type); + this.action = action; + this.lights = lights; + this.state = state; } /** diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/Group.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/Group.java index f921af7a5bc6a..b841c42d42b2c 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/Group.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/Group.java @@ -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; } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBridge.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBridge.java index cb60b099bbce9..d2ae873e6ca8f 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBridge.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBridge.java @@ -22,6 +22,7 @@ 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; @@ -115,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. * @@ -870,6 +882,7 @@ public List getScenes() throws IOException, ApiException { return e.getValue(); })// .filter(scene -> !scene.isRecycle())// + .sorted(Comparator.comparing(Scene::getGroupId))// .collect(Collectors.toList()); } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueThingHandlerFactory.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueThingHandlerFactory.java index 7e8d84720a707..1441dd9655ff5 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueThingHandlerFactory.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueThingHandlerFactory.java @@ -159,7 +159,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; } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/Scene.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/Scene.java index b60a66f21f0b9..9093b06299b9e 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/Scene.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/Scene.java @@ -39,6 +39,24 @@ public class Scene { private String groupId; private boolean recycle; + /** + * Default constructor for GSon. + */ + public Scene() { + super(); + } + + /** + * Test constructor + */ + Scene(String id, String name, String groupId, List lightIds, boolean recycle) { + this.id = id; + this.name = name; + this.groupId = groupId; + this.lightIds = lightIds; + this.recycle = recycle; + } + @NonNull public String getId() { return id; @@ -89,15 +107,52 @@ public boolean isRecycle() { return recycle; } + /** + * Creates a {@link StateOption} to display this scene, including the group that it belongs to. + *

+ * The display name is built with the following pattern: + *

    + *
  1. Human readable name of the scene if set. Otherwise, the ID is displayed
  2. + *
  3. Group for which the scene is defined
  4. + *
+ */ public StateOption toStateOption(Map groupNames) { StringBuilder stateOptionLabel = new StringBuilder(name); if (groupId != null && groupNames.containsKey(groupId)) { stateOptionLabel.append(" (").append(groupNames.get(groupId)).append(")"); } - if (!id.contentEquals(name)) { - stateOptionLabel.append(" [").append(id).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. + *

+ * According to the hue API, a scene is applicable to a group if either + *

    + *
  1. The scene is defined for the group
  2. + *
  3. All lights of the scene also belong to the group
  4. + *
+ */ + public boolean isApplicableTo(FullGroup group) { + if (getGroupId() == null) { + return getLightIds().parallelStream()// + .allMatch(id -> group.getLights().contains(id)); + } else { + return group.getId().contentEquals(getGroupId()); + } + } + + @Override + public String toString() { + return String.format("{Scene name: %s; id: %s; lightIds: %s; groupId: %s; recycle: %s}", name, id, lightIds, + groupId, recycle); + } } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueLightDiscoveryService.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueLightDiscoveryService.java index 161343b9ec928..cfa47af091d5d 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueLightDiscoveryService.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/discovery/HueLightDiscoveryService.java @@ -37,6 +37,7 @@ import org.openhab.binding.hue.internal.FullLight; import org.openhab.binding.hue.internal.FullSensor; import org.openhab.binding.hue.internal.HueBridge; +import org.openhab.binding.hue.internal.Scene; import org.openhab.binding.hue.internal.handler.GroupStatusListener; import org.openhab.binding.hue.internal.handler.HueBridgeHandler; import org.openhab.binding.hue.internal.handler.HueGroupHandler; @@ -326,4 +327,9 @@ public void onGroupStateChanged(@Nullable HueBridge bridge, FullGroup group) { // nothing to do } + @Override + public void onScenesUpdated(@Nullable HueBridge bridge, List scenes) { + // nothing to do + } + } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/GroupStatusListener.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/GroupStatusListener.java index cccc27840134e..a0c2558d38deb 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/GroupStatusListener.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/GroupStatusListener.java @@ -12,10 +12,13 @@ */ package org.openhab.binding.hue.internal.handler; +import java.util.List; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.hue.internal.FullGroup; import org.openhab.binding.hue.internal.HueBridge; +import org.openhab.binding.hue.internal.Scene; /** * The {@link GroupStatusListener} is notified when a group status has changed or a group has been removed or added. @@ -57,4 +60,12 @@ public interface GroupStatusListener { * @param group The added group */ void onGroupAdded(@Nullable HueBridge bridge, FullGroup group); + + /** + * This method is called whenever the list of available scenes is updated. + * + * @param bridge The bridge on which all scenes is stored + * @param updatedScenes available scenes + */ + void onScenesUpdated(@Nullable HueBridge bridge, List scenes); } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java index a903ce614429c..24c2485504cbf 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java @@ -56,6 +56,7 @@ import org.openhab.binding.hue.internal.FullSensor; import org.openhab.binding.hue.internal.HueBridge; import org.openhab.binding.hue.internal.HueConfigStatusMessage; +import org.openhab.binding.hue.internal.Scene; import org.openhab.binding.hue.internal.State; import org.openhab.binding.hue.internal.StateUpdate; import org.openhab.binding.hue.internal.config.HueBridgeConfig; @@ -98,7 +99,7 @@ private static enum StatusType { } private final Logger logger = LoggerFactory.getLogger(HueBridgeHandler.class); - private final @NonNullByDefault({}) HueStateDescriptionOptionProvider stateDescriptionOptionProvider; + private final HueStateDescriptionOptionProvider stateDescriptionOptionProvider; private final Map lastLightStates = new ConcurrentHashMap<>(); private final Map lastSensorStates = new ConcurrentHashMap<>(); @@ -322,9 +323,17 @@ private void updateGroups() throws IOException, ApiException { private final Runnable scenePollingRunnable = new PollingRunnable() { @Override protected void doConnectedRun() throws IOException, ApiException { - Map groupNames = lastGroupStates.entrySet().parallelStream() + List scenes = hueBridge.getScenes(); + logger.trace("Scenes detected: {}", scenes); + + setBridgeSceneChannelStateOptions(scenes, lastGroupStates); + notifyGroupSceneUpdate(scenes); + } + + private void setBridgeSceneChannelStateOptions(List scenes, Map groups) { + Map groupNames = groups.entrySet().parallelStream() .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getName())); - List stateOptions = hueBridge.getScenes().parallelStream()// + List stateOptions = scenes.parallelStream()// .map(scene -> scene.toStateOption(groupNames))// .collect(Collectors.toList()); stateDescriptionOptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SCENE), @@ -934,6 +943,10 @@ private void notifyGroupStatusListeners(final FullGroup fullGroup, StatusType ty } } + private void notifyGroupSceneUpdate(List scenes) { + groupStatusListeners.forEach(l -> l.onScenesUpdated(hueBridge, scenes)); + } + @Override public Collection getConfigStatus() { // The bridge IP address to be used for checks diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueGroupHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueGroupHandler.java index 4d77e28dcbfc8..e8a480a5c5da9 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueGroupHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueGroupHandler.java @@ -16,7 +16,9 @@ import java.math.BigDecimal; import java.util.Collections; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -34,9 +36,11 @@ import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; import org.eclipse.smarthome.core.thing.binding.ThingHandler; import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.StateOption; import org.eclipse.smarthome.core.types.UnDefType; import org.openhab.binding.hue.internal.FullGroup; import org.openhab.binding.hue.internal.HueBridge; +import org.openhab.binding.hue.internal.Scene; import org.openhab.binding.hue.internal.State; import org.openhab.binding.hue.internal.State.ColorMode; import org.openhab.binding.hue.internal.StateUpdate; @@ -54,6 +58,7 @@ public class HueGroupHandler extends BaseThingHandler implements GroupStatusList public static final Set SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_GROUP); private final Logger logger = LoggerFactory.getLogger(HueGroupHandler.class); + private final HueStateDescriptionOptionProvider stateDescriptionOptionProvider; private @NonNullByDefault({}) String groupId; @@ -64,8 +69,9 @@ public class HueGroupHandler extends BaseThingHandler implements GroupStatusList private @Nullable HueClient hueClient; - public HueGroupHandler(Thing thing) { + public HueGroupHandler(Thing thing, HueStateDescriptionOptionProvider stateDescriptionOptionProvider) { super(thing); + this.stateDescriptionOptionProvider = stateDescriptionOptionProvider; } @Override @@ -376,4 +382,17 @@ public void onGroupGone(@Nullable HueBridge bridge, FullGroup group) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "@text/offline.group-removed"); } } + + /** + * Sets the state options for applicable scenes. + */ + @Override + public void onScenesUpdated(@Nullable HueBridge bridge, List updatedScenes) { + List stateOptions = updatedScenes.parallelStream()// + .filter(scene -> scene.isApplicableTo(getHueClient().getGroupById(groupId)))// + .map(Scene::toStateOption)// + .collect(Collectors.toList()); + stateDescriptionOptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_SCENE), + stateOptions); + } } diff --git a/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/HueBridgeTest.java b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/HueBridgeTest.java new file mode 100644 index 0000000000000..b9e4005bcdaad --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/HueBridgeTest.java @@ -0,0 +1,99 @@ +/** + * 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 static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +import org.junit.Test; +import org.mockito.Mockito; +import org.openhab.binding.hue.internal.HttpClient.Result; +import org.openhab.binding.hue.internal.exceptions.ApiException; + +/** + * @author Hengrui Jiang - initial contribution + */ +public class HueBridgeTest { + + @Test + public void testGetScenes_ExcludeRecycleScenes() throws IOException, ApiException { + HttpClient mockHttpClient = Mockito.mock(HttpClient.class); + + HueBridge hueBridge = new HueBridge("ip", "baseUrl", "username", Executors.newScheduledThreadPool(1), + mockHttpClient); + + List testScenes = Arrays.asList(new Scene("id1", "name1", "group1", Collections.emptyList(), true), // + new Scene("id2", "name2", "group2", Collections.emptyList(), false)); + when(mockHttpClient.get("baseUrl/username/scenes")).thenReturn(new Result(createMockResponse(testScenes), 200)); + + List scenes = hueBridge.getScenes(); + assertThat(scenes.size(), is(1)); + assertThat(scenes.get(0).getId(), is("id2")); + } + + @Test + public void testGetScenes_OrderByGroup() throws IOException, ApiException { + HttpClient mockHttpClient = Mockito.mock(HttpClient.class); + + HueBridge hueBridge = new HueBridge("ip", "baseUrl", "username", Executors.newScheduledThreadPool(1), + mockHttpClient); + + List testScenes = Arrays.asList(new Scene("id1", "name1", "group1", Collections.emptyList(), false), // + new Scene("id2", "name2", "group2", Collections.emptyList(), false), + new Scene("id3", "name3", "group1", Collections.emptyList(), false)); + when(mockHttpClient.get("baseUrl/username/scenes")).thenReturn(new Result(createMockResponse(testScenes), 200)); + + List scenes = hueBridge.getScenes(); + assertThat(scenes.size(), is(3)); + assertThat(scenes.get(0).getId(), is("id1")); + assertThat(scenes.get(1).getId(), is("id3")); + assertThat(scenes.get(2).getId(), is("id2")); + } + + private static String createMockResponse(List scenes) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("{"); + stringBuilder.append(scenes.stream().map(HueBridgeTest::createMockJson).collect(Collectors.joining(","))); + stringBuilder.append("\n}"); + return stringBuilder.toString(); + } + + private static String createMockJson(Scene scene) { + // Sample response for getting scenes taken from hue API documentation. + // Extended with the attribute "group" + String template = "" + // + " \"%s\": {\n" + // + " \"name\": \"%s\",\n" + // + " \"lights\": [%s],\n" + // + " \"owner\": \"ffffffffe0341b1b376a2389376a2389\",\n" + // + " \"recycle\": %s,\n" + // + " \"locked\": false,\n" + // + " \"appdata\": {},\n" + // + " \"picture\": \"\",\n" + // + " \"lastupdated\": \"2015-12-03T08:57:13\",\n" + // + " \"version\": 2,\n" + // + " \"group\": \"%s\"\n" + // + " }"; + String lights = String.join(",", + scene.getLightIds().stream().map(id -> "\"" + id + "\"").collect(Collectors.toList())); + return String.format(template, scene.getId(), scene.getName(), lights, scene.isRecycle(), scene.getGroupId()); + } +} diff --git a/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/SceneTest.java b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/SceneTest.java new file mode 100644 index 0000000000000..2782b9c054ff9 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/SceneTest.java @@ -0,0 +1,108 @@ +/** + * 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 static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; + +/** + * @author HJiang - initial contribution + */ +public class SceneTest { + + private static final State PLACEHOLDER_STATE = new State(); + private static final String PLACEHOLDER = "placeholder"; + + /** + * If a scene already has a group ID, it should applicable to the group with the given ID. + */ + @Test + public void testIsApplicableTo_HasGroupId_MatchingGroup() { + String groupId = "groupId"; + List lights = Arrays.asList("1", "2"); + + Scene scene = new Scene(PLACEHOLDER, PLACEHOLDER, groupId, lights, false); + FullGroup group = new FullGroup(groupId, PLACEHOLDER, PLACEHOLDER, PLACEHOLDER_STATE, lights, + PLACEHOLDER_STATE); + + assertThat(scene.isApplicableTo(group), is(true)); + } + + /** + * If a scene already has a group ID, it should be NOT applicable to a group with different ID even if the lights + * match. + */ + @Test + public void testIsApplicableTo_HasGroupId_NotMatchingGroup() { + String groupId = "groupId"; + String otherGroupId = "otherGroupId"; + List lights = Arrays.asList("1", "2"); + List otherLights = Arrays.asList("1", "2", "3"); + + Scene scene = new Scene(PLACEHOLDER, PLACEHOLDER, groupId, lights, false); + + FullGroup nonMatchingGroupWithOtherLights = new FullGroup(otherGroupId, PLACEHOLDER, PLACEHOLDER, + PLACEHOLDER_STATE, otherLights, PLACEHOLDER_STATE); + assertThat(scene.isApplicableTo(nonMatchingGroupWithOtherLights), is(false)); + + FullGroup nonMatchingGroupWithSameLights = new FullGroup(otherGroupId, PLACEHOLDER, PLACEHOLDER, + PLACEHOLDER_STATE, lights, PLACEHOLDER_STATE); + assertThat(scene.isApplicableTo(nonMatchingGroupWithSameLights), is(false)); + } + + /** + * If a scene does not have a group ID, it should be applicable to a group that contains all lights of the + * scene. + */ + @Test + public void testIsApplicableTo_NoGroupId_SceneLightsContainedInGroup() { + List lights = Arrays.asList("1", "2"); + List moreLights = Arrays.asList("1", "2", "3"); + + Scene scene = new Scene(PLACEHOLDER, PLACEHOLDER, null, lights, false); + + FullGroup groupWithAllLights = new FullGroup("groupId", PLACEHOLDER, PLACEHOLDER, PLACEHOLDER_STATE, lights, + PLACEHOLDER_STATE); + assertThat(scene.isApplicableTo(groupWithAllLights), is(true)); + + FullGroup groupWithMoreLights = new FullGroup("otherGroupId", PLACEHOLDER, PLACEHOLDER, PLACEHOLDER_STATE, + moreLights, PLACEHOLDER_STATE); + assertThat(scene.isApplicableTo(groupWithMoreLights), is(true)); + } + + /** + * If a scene does not have a group ID, it should be NOT applicable to a group that does not contain all lights of + * the scene. + */ + @Test + public void testIsApplicableTo_NoGroupId_SceneLightsNotContainedInGroup() { + List lights = Arrays.asList("1", "2"); + List lessLights = Arrays.asList("1"); + List differentLights = Arrays.asList("3"); + + Scene scene = new Scene(PLACEHOLDER, PLACEHOLDER, null, lights, false); + + FullGroup groupWithLessLights = new FullGroup("groupId", PLACEHOLDER, PLACEHOLDER, PLACEHOLDER_STATE, + lessLights, PLACEHOLDER_STATE); + assertThat(scene.isApplicableTo(groupWithLessLights), is(false)); + + FullGroup groupWithDifferentLights = new FullGroup("otherGroupId", PLACEHOLDER, PLACEHOLDER, PLACEHOLDER_STATE, + differentLights, PLACEHOLDER_STATE); + assertThat(scene.isApplicableTo(groupWithDifferentLights), is(false)); + } +}