* Add support for scene collections.
Fixes #11533
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Add unit test for parsing of scene collections response.
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Add default i18n properties file.
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Fix CAT: File does not end with a newline.
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Update documentation with scene collections.
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Fix CAT: File does not end with a newline.
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Fix formatting.
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Fix CAT: File does not end with a newline.
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Split offline tests into separate distinct tests.
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Increase test coverage for scene/scene collection parsing.
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Internationalization of dynamic scene/scene collection channels.
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Rename scene collections to scene groups.
Renamed for all user-oriented texts/references to be consistent with now abandoned feature of the PowerView app.
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Change custom text keys to not collide with framework.
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Avoid multiple thing updates.
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Add missing label/description texts for secondary channel.
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Remove unneeded @Nullable annotations.
Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
### Channels for PowerView Hub
-Scene channels will be added dynamically to the binding as they are discovered in the hub.
-Each scene channel will have an entry in the hub as shown below, whereby different scenes have different `id` values:
+Scene and scene group channels will be added dynamically to the binding as they are discovered in the hub.
+Each scene/scene group channel will have an entry in the hub as shown below, whereby different scenes/scene groups
+have different `id` values:
| Channel | Item Type | Description |
|----------|-----------| ------------|
-| id | Switch | Turning this to ON will activate the scene. Scenes are stateless in the PowerView hub; they have no on/off state. Note: include `{autoupdate="false"}` in the item configuration to avoid having to reset it to off after use. |
+| id | Switch | Turning this to ON will activate the scene/scene group. Scenes/scene groups are stateless in the PowerView hub; they have no on/off state. Note: include `{autoupdate="false"}` in the item configuration to avoid having to reset it to off after use. |
### Channels for PowerView Shade
*
* @author Andy Lintner - Initial contribution
* @author Andrew Fiddian-Green - Added support for secondary rail positions
+ * @author Jacob Laursen - Add support for scene groups
*/
@NonNullByDefault
public class HDPowerViewBindingConstants {
public static final String CHANNEL_SHADE_SIGNAL_STRENGTH = "signalStrength";
public static final String CHANNELTYPE_SCENE_ACTIVATE = "scene-activate";
+ public static final String CHANNELTYPE_SCENE_GROUP_ACTIVATE = "scene-group-activate";
public static final List<String> NETBIOS_NAMES = Arrays.asList("PDBU-Hub3.0", "PowerView-Hub");
import org.openhab.binding.hdpowerview.internal.handler.HDPowerViewHubHandler;
import org.openhab.binding.hdpowerview.internal.handler.HDPowerViewShadeHandler;
import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.i18n.LocaleProvider;
+import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
public class HDPowerViewHandlerFactory extends BaseThingHandlerFactory {
private final HttpClient httpClient;
+ private final HDPowerViewTranslationProvider translationProvider;
@Activate
- public HDPowerViewHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
+ public HDPowerViewHandlerFactory(@Reference HttpClientFactory httpClientFactory,
+ final @Reference TranslationProvider i18nProvider, final @Reference LocaleProvider localeProvider,
+ ComponentContext componentContext) {
+ super.activate(componentContext);
this.httpClient = httpClientFactory.getCommonHttpClient();
+ this.translationProvider = new HDPowerViewTranslationProvider(getBundleContext().getBundle(), i18nProvider,
+ localeProvider);
}
@Override
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(HDPowerViewBindingConstants.THING_TYPE_HUB)) {
- HDPowerViewHubHandler handler = new HDPowerViewHubHandler((Bridge) thing, httpClient);
+ HDPowerViewHubHandler handler = new HDPowerViewHubHandler((Bridge) thing, httpClient, translationProvider);
registerService(new HDPowerViewShadeDiscoveryService(handler));
return handler;
} else if (thingTypeUID.equals(HDPowerViewBindingConstants.THING_TYPE_SHADE)) {
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.hdpowerview.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.i18n.LocaleProvider;
+import org.openhab.core.i18n.TranslationProvider;
+import org.osgi.framework.Bundle;
+
+/**
+ * {@link HDPowerViewTranslationProvider} provides i18n message lookup
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class HDPowerViewTranslationProvider {
+
+ private final Bundle bundle;
+ private final TranslationProvider i18nProvider;
+ private final LocaleProvider localeProvider;
+
+ public HDPowerViewTranslationProvider(Bundle bundle, TranslationProvider i18nProvider,
+ LocaleProvider localeProvider) {
+ this.bundle = bundle;
+ this.i18nProvider = i18nProvider;
+ this.localeProvider = localeProvider;
+ }
+
+ public String getText(String key, @Nullable Object... arguments) {
+ String text = i18nProvider.getText(bundle, key, key, localeProvider.getLocale(), arguments);
+ if (text != null) {
+ return text;
+ }
+ return key;
+ }
+}
import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
import org.openhab.binding.hdpowerview.internal.api.requests.ShadeMove;
import org.openhab.binding.hdpowerview.internal.api.requests.ShadeStop;
+import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections;
import org.openhab.binding.hdpowerview.internal.api.responses.Scenes;
import org.openhab.binding.hdpowerview.internal.api.responses.Shade;
import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
*
* @author Andy Lintner - Initial contribution
* @author Andrew Fiddian-Green - Added support for secondary rail positions
+ * @author Jacob Laursen - Add support for scene groups
*/
@NonNullByDefault
public class HDPowerViewWebTargets {
private final String shades;
private final String sceneActivate;
private final String scenes;
+ private final String sceneCollectionActivate;
+ private final String sceneCollections;
private final Gson gson = new Gson();
private final HttpClient httpClient;
shades = base + "shades/";
sceneActivate = base + "scenes";
scenes = base + "scenes/";
+ sceneCollectionActivate = base + "sceneCollections";
+ sceneCollections = base + "sceneCollections/";
this.httpClient = httpClient;
}
invoke(HttpMethod.GET, sceneActivate, Query.of("sceneId", Integer.toString(sceneId)), null);
}
+ /**
+ * Fetches a JSON package that describes all scene collections in the hub, and wraps it in
+ * a SceneCollections class instance
+ *
+ * @return SceneCollections class instance
+ * @throws JsonParseException if there is a JSON parsing error
+ * @throws HubProcessingException if there is any processing error
+ * @throws HubMaintenanceException if the hub is down for maintenance
+ */
+ public @Nullable SceneCollections getSceneCollections()
+ throws JsonParseException, HubProcessingException, HubMaintenanceException {
+ String json = invoke(HttpMethod.GET, sceneCollections, null, null);
+ return gson.fromJson(json, SceneCollections.class);
+ }
+
+ /**
+ * Instructs the hub to execute a specific scene collection
+ *
+ * @param sceneCollectionId id of the scene collection to be executed
+ * @throws HubProcessingException if there is any processing error
+ * @throws HubMaintenanceException if the hub is down for maintenance
+ */
+ public void activateSceneCollection(int sceneCollectionId) throws HubProcessingException, HubMaintenanceException {
+ invoke(HttpMethod.GET, sceneCollectionActivate,
+ Query.of("sceneCollectionId", Integer.toString(sceneCollectionId)), null);
+ }
+
/**
* Invoke a call on the hub server to retrieve information or send a command
*
--- /dev/null
+/**
+ * Copyright (c) 2010-2021 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.hdpowerview.internal.api.responses;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * State of all Scenes in an HD PowerView hub
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class SceneCollections {
+
+ public @Nullable List<SceneCollection> sceneCollectionData;
+ public @Nullable List<Integer> sceneCollectionIds;
+
+ /*
+ * the following SuppressWarnings annotation is because the Eclipse compiler
+ * does NOT expect a NonNullByDefault annotation on the inner class, since it is
+ * implicitly inherited from the outer class, whereas the Maven compiler always
+ * requires an explicit NonNullByDefault annotation on all classes
+ */
+ @SuppressWarnings("null")
+ @NonNullByDefault
+ public static class SceneCollection {
+ public int id;
+ public @Nullable String name;
+ public int order;
+ public int colorId;
+ public int iconId;
+
+ public String getName() {
+ return new String(Base64.getDecoder().decode(name), StandardCharsets.UTF_8);
+ }
+ }
+}
*/
package org.openhab.binding.hdpowerview.internal.api.responses;
+import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
public int iconId;
public String getName() {
- return new String(Base64.getDecoder().decode(name));
+ return new String(Base64.getDecoder().decode(name), StandardCharsets.UTF_8);
}
}
}
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants;
+import org.openhab.binding.hdpowerview.internal.HDPowerViewTranslationProvider;
import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets;
import org.openhab.binding.hdpowerview.internal.HubMaintenanceException;
import org.openhab.binding.hdpowerview.internal.HubProcessingException;
+import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections;
+import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections.SceneCollection;
import org.openhab.binding.hdpowerview.internal.api.responses.Scenes;
import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene;
import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
*
* @author Andy Lintner - Initial contribution
* @author Andrew Fiddian-Green - Added support for secondary rail positions
+ * @author Jacob Laursen - Add support for scene groups
*/
@NonNullByDefault
public class HDPowerViewHubHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(HDPowerViewHubHandler.class);
private final HttpClient httpClient;
+ private final HDPowerViewTranslationProvider translationProvider;
private long refreshInterval;
private long hardRefreshPositionInterval;
private final ChannelTypeUID sceneChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
HDPowerViewBindingConstants.CHANNELTYPE_SCENE_ACTIVATE);
- public HDPowerViewHubHandler(Bridge bridge, HttpClient httpClient) {
+ private final ChannelTypeUID sceneCollectionChannelTypeUID = new ChannelTypeUID(
+ HDPowerViewBindingConstants.BINDING_ID, HDPowerViewBindingConstants.CHANNELTYPE_SCENE_GROUP_ACTIVATE);
+
+ public HDPowerViewHubHandler(Bridge bridge, HttpClient httpClient,
+ HDPowerViewTranslationProvider translationProvider) {
super(bridge);
this.httpClient = httpClient;
+ this.translationProvider = translationProvider;
}
@Override
return;
}
+ if (!OnOffType.ON.equals(command)) {
+ return;
+ }
+
Channel channel = getThing().getChannel(channelUID.getId());
- if (channel != null && sceneChannelTypeUID.equals(channel.getChannelTypeUID())) {
- if (OnOffType.ON.equals(command)) {
- try {
- HDPowerViewWebTargets webTargets = this.webTargets;
- if (webTargets == null) {
- throw new ProcessingException("Web targets not initialized");
- }
- webTargets.activateScene(Integer.parseInt(channelUID.getId()));
- } catch (HubMaintenanceException e) {
- // exceptions are logged in HDPowerViewWebTargets
- } catch (NumberFormatException | HubProcessingException e) {
- logger.debug("Unexpected error {}", e.getMessage());
- }
+ if (channel == null) {
+ return;
+ }
+
+ try {
+ HDPowerViewWebTargets webTargets = this.webTargets;
+ if (webTargets == null) {
+ throw new ProcessingException("Web targets not initialized");
}
+ int id = Integer.parseInt(channelUID.getId());
+ if (sceneChannelTypeUID.equals(channel.getChannelTypeUID())) {
+ webTargets.activateScene(id);
+ } else if (sceneCollectionChannelTypeUID.equals(channel.getChannelTypeUID())) {
+ webTargets.activateSceneCollection(id);
+ }
+ } catch (HubMaintenanceException e) {
+ // exceptions are logged in HDPowerViewWebTargets
+ } catch (NumberFormatException | HubProcessingException e) {
+ logger.debug("Unexpected error {}", e.getMessage());
}
}
String host = config.host;
if (host == null || host.isEmpty()) {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Host address must be set");
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "@text/offline.conf-error-no-host-address");
return;
}
logger.debug("Polling for state");
pollShades();
pollScenes();
+ pollSceneCollections();
} catch (JsonParseException e) {
logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
} catch (HubProcessingException e) {
}
logger.debug("Received data for {} scenes", sceneData.size());
- Map<String, Channel> idChannelMap = getIdChannelMap();
+ Map<String, Channel> idChannelMap = getIdSceneChannelMap();
+ List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
+ boolean isChannelListChanged = false;
for (Scene scene : sceneData) {
// remove existing scene channel from the map
String sceneId = Integer.toString(scene.id);
} else {
// create a new scene channel
ChannelUID channelUID = new ChannelUID(getThing().getUID(), sceneId);
+ String description = translationProvider.getText("dynamic-channel.scene-activate.description",
+ scene.getName());
Channel channel = ChannelBuilder.create(channelUID, "Switch").withType(sceneChannelTypeUID)
- .withLabel(scene.getName()).withDescription("Activates the scene " + scene.getName()).build();
- updateThing(editThing().withChannel(channel).build());
+ .withLabel(scene.getName()).withDescription(description).build();
+ allChannels.add(channel);
+ isChannelListChanged = true;
logger.debug("Creating new channel for scene '{}'", sceneId);
}
}
// remove any previously created channels that no longer exist
if (!idChannelMap.isEmpty()) {
logger.debug("Removing {} orphan scene channels", idChannelMap.size());
- List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
allChannels.removeAll(idChannelMap.values());
+ isChannelListChanged = true;
+ }
+
+ if (isChannelListChanged) {
+ updateThing(editThing().withChannels(allChannels).build());
+ }
+ }
+
+ private void pollSceneCollections() throws JsonParseException, HubProcessingException, HubMaintenanceException {
+ HDPowerViewWebTargets webTargets = this.webTargets;
+ if (webTargets == null) {
+ throw new ProcessingException("Web targets not initialized");
+ }
+
+ SceneCollections sceneCollections = webTargets.getSceneCollections();
+ if (sceneCollections == null) {
+ throw new JsonParseException("Missing 'sceneCollections' element");
+ }
+
+ List<SceneCollection> sceneCollectionData = sceneCollections.sceneCollectionData;
+ if (sceneCollectionData == null) {
+ throw new JsonParseException("Missing 'sceneCollections.sceneCollectionData' element");
+ }
+ logger.debug("Received data for {} sceneCollections", sceneCollectionData.size());
+
+ Map<String, Channel> idChannelMap = getIdSceneCollectionChannelMap();
+ List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
+ boolean isChannelListChanged = false;
+ for (SceneCollection sceneCollection : sceneCollectionData) {
+ // remove existing scene collection channel from the map
+ String sceneCollectionId = Integer.toString(sceneCollection.id);
+ if (idChannelMap.containsKey(sceneCollectionId)) {
+ idChannelMap.remove(sceneCollectionId);
+ logger.debug("Keeping channel for existing scene collection '{}'", sceneCollectionId);
+ } else {
+ // create a new scene collection channel
+ ChannelUID channelUID = new ChannelUID(getThing().getUID(), sceneCollectionId);
+ String description = translationProvider.getText("dynamic-channel.scene-group-activate.description",
+ sceneCollection.getName());
+ Channel channel = ChannelBuilder.create(channelUID, "Switch").withType(sceneCollectionChannelTypeUID)
+ .withLabel(sceneCollection.getName()).withDescription(description).build();
+ allChannels.add(channel);
+ isChannelListChanged = true;
+ logger.debug("Creating new channel for scene collection '{}'", sceneCollectionId);
+ }
+ }
+
+ // remove any previously created channels that no longer exist
+ if (!idChannelMap.isEmpty()) {
+ logger.debug("Removing {} orphan scene collection channels", idChannelMap.size());
+ allChannels.removeAll(idChannelMap.values());
+ isChannelListChanged = true;
+ }
+
+ if (isChannelListChanged) {
updateThing(editThing().withChannels(allChannels).build());
}
}
return ret;
}
- private Map<String, Channel> getIdChannelMap() {
+ private Map<String, Channel> getIdSceneChannelMap() {
Map<String, Channel> ret = new HashMap<>();
for (Channel channel : getThing().getChannels()) {
if (sceneChannelTypeUID.equals(channel.getChannelTypeUID())) {
return ret;
}
+ private Map<String, Channel> getIdSceneCollectionChannelMap() {
+ Map<String, Channel> ret = new HashMap<>();
+ for (Channel channel : getThing().getChannels()) {
+ if (sceneCollectionChannelTypeUID.equals(channel.getChannelTypeUID())) {
+ ret.put(channel.getUID().getId(), channel);
+ }
+ }
+ return ret;
+ }
+
private void requestRefreshShadePositions() {
Map<Thing, String> thingIdMap = getThingIdMap();
for (Entry<Thing, String> item : thingIdMap.entrySet()) {
--- /dev/null
+# binding
+
+binding.hdpowerview.name = Hunter Douglas PowerView Binding
+binding.hdpowerview.description = The Hunter Douglas PowerView binding provides access to the Hunter Douglas line of PowerView shades.
+
+# thing types
+
+thing-type.hdpowerview.hub.label = PowerView Hub
+thing-type.hdpowerview.hub.description = Hunter Douglas (Luxaflex) PowerView Hub
+thing-type.hdpowerview.shade.label = PowerView Shade
+thing-type.hdpowerview.shade.description = Hunter Douglas (Luxaflex) PowerView Shade
+thing-type.hdpowerview.shade.channel.secondary.label = Secondary Position
+thing-type.hdpowerview.shade.channel.secondary.description = The secondary vertical position (on top-down/bottom-up shades)
+
+# thing types config
+
+thing-type.config.hdpowerview.hub.hardRefresh.label = Hard Position Refresh Interval
+thing-type.config.hdpowerview.hub.hardRefresh.description = The number of minutes between hard refreshes of positions from the PowerView Hub (or 0 to disable)
+thing-type.config.hdpowerview.hub.hardRefreshBatteryLevel.label = Hard Battery Level Refresh Interval
+thing-type.config.hdpowerview.hub.hardRefreshBatteryLevel.description = The number of hours between hard refreshes of battery levels from the PowerView Hub (or 0 to disable, default is weekly)
+thing-type.config.hdpowerview.hub.host.label = Host
+thing-type.config.hdpowerview.hub.host.description = The Host address of the PowerView Hub
+thing-type.config.hdpowerview.hub.refresh.label = Refresh Interval
+thing-type.config.hdpowerview.hub.refresh.description = The number of milliseconds between fetches of the PowerView Hub shade state
+thing-type.config.hdpowerview.shade.id.label = ID
+thing-type.config.hdpowerview.shade.id.description = The numeric ID of the PowerView Shade in the Hub
+
+# channel types
+
+channel-type.hdpowerview.battery-voltage.label = Battery Voltage
+channel-type.hdpowerview.battery-voltage.description = Battery voltage reported by the shade
+channel-type.hdpowerview.shade-position.label = Position
+channel-type.hdpowerview.shade-position.description = The vertical position of the shade
+channel-type.hdpowerview.shade-vane.label = Vane
+channel-type.hdpowerview.shade-vane.description = The opening of the slats in the shade
+
+# thing status descriptions
+
+offline.conf-error-no-host-address = Host address must be set
+
+# dynamic channels
+
+dynamic-channel.scene-activate.description = Activates the scene ''{0}''
+dynamic-channel.scene-group-activate.description = Activates the scene group ''{0}''
<channel-type id="scene-activate">
<item-type>Switch</item-type>
<label>Activate</label>
- <description>Activates the scene</description>
+ </channel-type>
+
+ <channel-type id="scene-group-activate">
+ <item-type>Switch</item-type>
+ <label>Activate</label>
</channel-type>
<channel-type id="battery-voltage" advanced="true">
import static org.openhab.binding.hdpowerview.internal.api.ActuatorClass.*;
import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*;
-import java.io.BufferedReader;
-import java.io.FileReader;
import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
import java.util.List;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.hdpowerview.internal.HubProcessingException;
import org.openhab.binding.hdpowerview.internal.api.CoordinateSystem;
import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
+import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections;
+import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections.SceneCollection;
import org.openhab.binding.hdpowerview.internal.api.responses.Scenes;
import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene;
import org.openhab.binding.hdpowerview.internal.api.responses.Shade;
* Unit tests for HD PowerView binding
*
* @author Andrew Fiddian-Green - Initial contribution
+ * @author Jacob Laursen - Add support for scene groups
*/
@NonNullByDefault
public class HDPowerViewJUnitTests {
* load a test JSON string from a file
*/
private String loadJson(String fileName) {
- try (FileReader file = new FileReader(String.format("src/test/resources/%s.json", fileName));
- BufferedReader reader = new BufferedReader(file)) {
- StringBuilder builder = new StringBuilder();
- String line;
- while ((line = reader.readLine()) != null) {
- builder.append(line).append("\n");
- }
- return builder.toString();
+ try {
+ return Files.readAllLines(Paths.get(String.format("src/test/resources/%s.json", fileName))).stream()
+ .collect(Collectors.joining());
} catch (IOException e) {
fail(e.getMessage());
}
}
/**
- * Run a series of OFFLINE tests on the JSON parsing machinery
+ * Test generic JSON shades response
*/
@Test
- public void testOfflineJsonParsing() {
+ public void shadeResponseIsParsedCorrectly() throws JsonParseException {
final Gson gson = new Gson();
-
@Nullable
Shades shades;
- // test generic JSON shades response
- try {
- @Nullable
- String json = loadJson("shades");
- assertNotNull(json);
- assertNotEquals("", json);
- shades = gson.fromJson(json, Shades.class);
- assertNotNull(shades);
- } catch (JsonParseException e) {
- fail(e.getMessage());
- }
+ String json = loadJson("shades");
+ assertNotEquals("", json);
+ shades = gson.fromJson(json, Shades.class);
+ assertNotNull(shades);
+ }
- // test generic JSON scenes response
- try {
- @Nullable
- String json = loadJson("scenes");
- assertNotNull(json);
- assertNotEquals("", json);
- @Nullable
- Scenes scenes = gson.fromJson(json, Scenes.class);
- assertNotNull(scenes);
- } catch (JsonParseException e) {
- fail(e.getMessage());
- }
+ /**
+ * Test generic JSON scene response
+ */
+ @Test
+ public void sceneResponseIsParsedCorrectly() throws JsonParseException {
+ final Gson gson = new Gson();
+ String json = loadJson("scenes");
+ assertNotEquals("", json);
- // test the JSON parsing for a duette top down bottom up shade
- try {
- @Nullable
- ShadeData shadeData = null;
- String json = loadJson("duette");
- assertNotNull(json);
- assertNotEquals("", json);
+ @Nullable
+ Scenes scenes = gson.fromJson(json, Scenes.class);
+ assertNotNull(scenes);
- shades = gson.fromJson(json, Shades.class);
- assertNotNull(shades);
- @Nullable
- List<ShadeData> shadesData = shades.shadeData;
- assertNotNull(shadesData);
+ @Nullable
+ List<Scene> sceneData = scenes.sceneData;
+ assertNotNull(sceneData);
- assertEquals(1, shadesData.size());
- shadeData = shadesData.get(0);
- assertNotNull(shadeData);
+ assertEquals(4, sceneData.size());
+ @Nullable
+ Scene scene = sceneData.get(0);
+ assertEquals("Door Open", scene.getName());
+ assertEquals(18097, scene.id);
+ }
- assertEquals("Gardin 1", shadeData.getName());
- assertEquals(63778, shadeData.id);
+ /**
+ * Test generic JSON scene collection response
+ */
+ @Test
+ public void sceneCollectionResponseIsParsedCorrectly() throws JsonParseException {
+ final Gson gson = new Gson();
+ String json = loadJson("sceneCollections");
+ assertNotEquals("", json);
- ShadePosition shadePos = shadeData.positions;
- assertNotNull(shadePos);
- assertEquals(ZERO_IS_CLOSED, shadePos.getCoordinateSystem(PRIMARY_ACTUATOR));
+ @Nullable
+ SceneCollections sceneCollections = gson.fromJson(json, SceneCollections.class);
+ assertNotNull(sceneCollections);
+ @Nullable
+ List<SceneCollection> sceneCollectionData = sceneCollections.sceneCollectionData;
+ assertNotNull(sceneCollectionData);
- State pos = shadePos.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED);
- assertEquals(PercentType.class, pos.getClass());
- assertEquals(59, ((PercentType) pos).intValue());
+ assertEquals(1, sceneCollectionData.size());
+ @Nullable
+ SceneCollection sceneCollection = sceneCollectionData.get(0);
+ assertEquals("Børn op", sceneCollection.getName());
+ assertEquals(27119, sceneCollection.id);
+ }
- pos = shadePos.getState(SECONDARY_ACTUATOR, ZERO_IS_OPEN);
- assertEquals(PercentType.class, pos.getClass());
- assertEquals(35, ((PercentType) pos).intValue());
+ /**
+ * Test the JSON parsing for a duette top down bottom up shade
+ */
+ @Test
+ public void duetteTopDownBottomUpShadeIsParsedCorrectly() throws JsonParseException {
+ final Gson gson = new Gson();
+ String json = loadJson("duette");
+ assertNotEquals("", json);
- pos = shadePos.getState(PRIMARY_ACTUATOR, VANE_COORDS);
- assertEquals(UnDefType.class, pos.getClass());
+ @Nullable
+ Shades shades = gson.fromJson(json, Shades.class);
+ assertNotNull(shades);
+ @Nullable
+ List<ShadeData> shadesData = shades.shadeData;
+ assertNotNull(shadesData);
- assertEquals(3, shadeData.batteryStatus);
+ assertEquals(1, shadesData.size());
+ @Nullable
+ ShadeData shadeData = shadesData.get(0);
+ assertNotNull(shadeData);
- assertEquals(4, shadeData.signalStrength);
- } catch (JsonParseException e) {
- fail(e.getMessage());
- }
+ assertEquals("Gardin 1", shadeData.getName());
+ assertEquals(63778, shadeData.id);
+
+ ShadePosition shadePos = shadeData.positions;
+ assertNotNull(shadePos);
+ assertEquals(ZERO_IS_CLOSED, shadePos.getCoordinateSystem(PRIMARY_ACTUATOR));
+
+ State pos = shadePos.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED);
+ assertEquals(PercentType.class, pos.getClass());
+ assertEquals(59, ((PercentType) pos).intValue());
+
+ pos = shadePos.getState(SECONDARY_ACTUATOR, ZERO_IS_OPEN);
+ assertEquals(PercentType.class, pos.getClass());
+ assertEquals(35, ((PercentType) pos).intValue());
+
+ pos = shadePos.getState(PRIMARY_ACTUATOR, VANE_COORDS);
+ assertEquals(UnDefType.class, pos.getClass());
+
+ assertEquals(3, shadeData.batteryStatus);
+
+ assertEquals(4, shadeData.signalStrength);
}
}
--- /dev/null
+{
+ "sceneCollectionIds": [
+ 27119
+ ],
+ "sceneCollectionData": [
+ {
+ "name": "QsO4cm4gb3A=",
+ "colorId": 12,
+ "iconId": 17,
+ "id": 27119,
+ "order": 0,
+ "hkAssist": false
+ }
+ ]
+}