]> git.basschouten.com Git - openhab-addons.git/commitdiff
[hdpowerview] Add support for scene groups (#11534)
authorjlaur <jacob-github@vindvejr.dk>
Mon, 15 Nov 2021 22:53:23 +0000 (23:53 +0100)
committerGitHub <noreply@github.com>
Mon, 15 Nov 2021 22:53:23 +0000 (23:53 +0100)
* 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>
12 files changed:
bundles/org.openhab.binding.hdpowerview/README.md
bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewBindingConstants.java
bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewHandlerFactory.java
bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewTranslationProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java
bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/SceneCollections.java [new file with mode: 0644]
bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Scenes.java
bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java
bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/i18n/hdpowerview.properties [new file with mode: 0644]
bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/thing/thing-types.xml
bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/HDPowerViewJUnitTests.java
bundles/org.openhab.binding.hdpowerview/src/test/resources/sceneCollections.json [new file with mode: 0644]

index d387c46160d5b0f2888302b680e8e458b27d8c2e..60601cf6c2ec6809e9d19c37d102878000751f00 100644 (file)
@@ -60,12 +60,13 @@ However, the configuration parameters are described below:
 
 ### 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
 
index da235dd22c8dafa5b10e450ffc1c98eeddc1db6c..6eb621157351eb5a05da079bcfd5ca5b26993296 100644 (file)
@@ -26,6 +26,7 @@ import org.openhab.core.thing.ThingTypeUID;
  *
  * @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 {
@@ -46,6 +47,7 @@ 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");
 
index 8b4dc8aeb8be191c55e000cc7f91d57cf89d6b47..65083143bed5698ab77ff86c18d3eed2f273157d 100644 (file)
@@ -21,6 +21,8 @@ import org.openhab.binding.hdpowerview.internal.discovery.HDPowerViewShadeDiscov
 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;
@@ -28,6 +30,7 @@ import org.openhab.core.thing.ThingTypeUID;
 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;
@@ -43,10 +46,16 @@ 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
@@ -59,7 +68,7 @@ public class HDPowerViewHandlerFactory extends BaseThingHandlerFactory {
         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)) {
diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewTranslationProvider.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewTranslationProvider.java
new file mode 100644 (file)
index 0000000..adf1605
--- /dev/null
@@ -0,0 +1,47 @@
+/**
+ * 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;
+    }
+}
index eaa17cfbfe3acbd045c60f9c163fa4bba8aa26c8..0afa08478341a85ec5029fcc1f38359df0ac8889 100644 (file)
@@ -28,6 +28,7 @@ import org.eclipse.jetty.http.HttpStatus;
 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;
@@ -42,6 +43,7 @@ import com.google.gson.JsonParseException;
  *
  * @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 {
@@ -61,6 +63,8 @@ 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;
@@ -101,6 +105,8 @@ public class HDPowerViewWebTargets {
         shades = base + "shades/";
         sceneActivate = base + "scenes";
         scenes = base + "scenes/";
+        sceneCollectionActivate = base + "sceneCollections";
+        sceneCollections = base + "sceneCollections/";
         this.httpClient = httpClient;
     }
 
@@ -156,6 +162,33 @@ public class HDPowerViewWebTargets {
         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
      *
diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/SceneCollections.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/SceneCollections.java
new file mode 100644 (file)
index 0000000..f4822b0
--- /dev/null
@@ -0,0 +1,52 @@
+/**
+ * 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);
+        }
+    }
+}
index 49664b0dbc57fae746a6c887b0d73ede7e55861d..cf759e84fc38908572bd37f3e4ad4be3bb54ab6c 100644 (file)
@@ -12,6 +12,7 @@
  */
 package org.openhab.binding.hdpowerview.internal.api.responses;
 
+import java.nio.charset.StandardCharsets;
 import java.util.Base64;
 import java.util.List;
 
@@ -46,7 +47,7 @@ public class Scenes {
         public int iconId;
 
         public String getName() {
-            return new String(Base64.getDecoder().decode(name));
+            return new String(Base64.getDecoder().decode(name), StandardCharsets.UTF_8);
         }
     }
 }
index d617bb06cfbeb3917aae692f5f9fb36a904ccf94..35f1696932efade9800d5e59009fd15de3e78288 100644 (file)
@@ -26,9 +26,12 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 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;
@@ -59,12 +62,14 @@ import com.google.gson.JsonParseException;
  *
  * @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;
@@ -78,9 +83,14 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
     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
@@ -90,21 +100,30 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
             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());
         }
     }
 
@@ -115,7 +134,8 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
         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;
         }
 
@@ -196,6 +216,7 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
             logger.debug("Polling for state");
             pollShades();
             pollScenes();
+            pollSceneCollections();
         } catch (JsonParseException e) {
             logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
         } catch (HubProcessingException e) {
@@ -266,7 +287,9 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
         }
         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);
@@ -276,9 +299,12 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
             } 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);
             }
         }
@@ -286,8 +312,62 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
         // 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());
         }
     }
@@ -313,7 +393,7 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
         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())) {
@@ -323,6 +403,16 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
         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()) {
diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/i18n/hdpowerview.properties b/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/i18n/hdpowerview.properties
new file mode 100644 (file)
index 0000000..34875ac
--- /dev/null
@@ -0,0 +1,44 @@
+# 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}''
index c852a49b752995c38c2856b3da08668d4f593ba4..22954d4097f202f4999d40020f95f5fe739ddd33 100644 (file)
        <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">
index 14eb6c0aaa595c977e372df25224ac9ef31631dc..8bd12d92b3a2de641ee61afa411c2c0814647307 100644 (file)
@@ -16,11 +16,12 @@ import static org.junit.jupiter.api.Assertions.*;
 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;
@@ -31,6 +32,8 @@ import org.openhab.binding.hdpowerview.internal.HubMaintenanceException;
 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;
@@ -47,6 +50,7 @@ import com.google.gson.JsonParseException;
  * Unit tests for HD PowerView binding
  *
  * @author Andrew Fiddian-Green - Initial contribution
+ * @author Jacob Laursen - Add support for scene groups
  */
 @NonNullByDefault
 public class HDPowerViewJUnitTests {
@@ -58,14 +62,9 @@ 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());
         }
@@ -287,80 +286,107 @@ public class HDPowerViewJUnitTests {
     }
 
     /**
-     * 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);
     }
 }
diff --git a/bundles/org.openhab.binding.hdpowerview/src/test/resources/sceneCollections.json b/bundles/org.openhab.binding.hdpowerview/src/test/resources/sceneCollections.json
new file mode 100644 (file)
index 0000000..7631f89
--- /dev/null
@@ -0,0 +1,15 @@
+{
+       "sceneCollectionIds": [
+               27119
+       ],
+       "sceneCollectionData": [
+               {
+                       "name": "QsO4cm4gb3A=",
+                       "colorId": 12,
+                       "iconId": 17,
+                       "id": 27119,
+                       "order": 0,
+                       "hkAssist": false
+               }
+       ]
+}