]> git.basschouten.com Git - openhab-addons.git/commitdiff
[hdpowerview] Add support for enabling/disabling automations (#11637)
authorjlaur <jacob-github@vindvejr.dk>
Sat, 11 Dec 2021 16:20:11 +0000 (17:20 +0100)
committerGitHub <noreply@github.com>
Sat, 11 Dec 2021 16:20:11 +0000 (17:20 +0100)
* Add support for enabling/disabling automations.

Fixes #11516

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Fix class description.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Document automation channel and channel groups.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Update scene example in documentation.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Consolidate method for getting channel map.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Extract channel updating from data fetching methods.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Draft implementation of better automation description.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Simplify and optimize building weekday string.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Further simplify building weekday string.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Update scheduled event channels when modified.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Update scene channels when modified.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Update scene group channels when modified.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Fix cache synchronization during initialization.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Reduced code duplication.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Shorten time formatting.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Danish translations for dynamic channels.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Simplify, optimize and fix dynamic channel creation.

Channel order is now preserved when updating an existing channel.

Scenes and scene collection are sorted correctly.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Provide backwards compatibility for deprecated channels.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Document purpose of createDeprecatedSceneChannels.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Cleaned up poll method for improved readability.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
* Fix potential race condition when initialize() is called while updating channels.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
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/HDPowerViewTranslationProvider.java
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
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/api/responses/ScheduledEvents.java [new file with mode: 0644]
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
bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/i18n/hdpowerview_da.properties [new file with mode: 0644]
bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/thing/thing-types.xml

index 60601cf6c2ec6809e9d19c37d102878000751f00..023e6213991169bb363ed531e1ec8b8fdc320dbc 100644 (file)
@@ -60,13 +60,15 @@ However, the configuration parameters are described below:
 
 ### Channels for PowerView Hub
 
-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
+Scene, scene group and automation channels will be added dynamically to the binding as they are discovered in the hub.
+Each will have an entry in the hub as shown below, whereby different scenes, scene groups and automations
 have different `id` values:
 
-| Channel  | Item Type | Description |
-|----------|-----------| ------------|
-| 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. |
+| Channel Group | Channel | Item Type | Description |
+|---------------|---------|-----------|-------------|
+| scenes        | id      | Switch    | Setting 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. |
+| sceneGroups   | id      | Switch    | Setting this to ON will activate the scene group. 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. |
+| automations   | id      | Switch    | Setting this to ON will enable the automation, while OFF will disable it. |
 
 ### Channels for PowerView Shade
 
@@ -181,7 +183,7 @@ Switch Living_Room_Shade_Battery_Low_Alarm "Living Room Shade Battery Low Alarm
 Scene items:
 
 ```
-Switch Living_Room_Shades_Scene_Heart "Living Room Shades Scene Heart" <blinds> (g_Shades_Scene_Trigger) {channel="hdpowerview:hub:g24:22663", autoupdate="false"}
+Switch Living_Room_Shades_Scene_Heart "Living Room Shades Scene Heart" <blinds> (g_Shades_Scene_Trigger) {channel="hdpowerview:hub:g24:scenes#22663", autoupdate="false"}
 ```
 
 ### `demo.sitemap` File
index 6eb621157351eb5a05da079bcfd5ca5b26993296..e4352e6a7b0982c02a9eda38199b5f934203f4e1 100644 (file)
@@ -26,7 +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
+ * @author Jacob Laursen - Add support for scene groups and automations
  */
 @NonNullByDefault
 public class HDPowerViewBindingConstants {
@@ -46,8 +46,13 @@ public class HDPowerViewBindingConstants {
     public static final String CHANNEL_SHADE_BATTERY_VOLTAGE = "batteryVoltage";
     public static final String CHANNEL_SHADE_SIGNAL_STRENGTH = "signalStrength";
 
+    public static final String CHANNEL_GROUP_SCENES = "scenes";
+    public static final String CHANNEL_GROUP_SCENE_GROUPS = "sceneGroups";
+    public static final String CHANNEL_GROUP_AUTOMATIONS = "automations";
+
     public static final String CHANNELTYPE_SCENE_ACTIVATE = "scene-activate";
     public static final String CHANNELTYPE_SCENE_GROUP_ACTIVATE = "scene-group-activate";
+    public static final String CHANNELTYPE_AUTOMATION_ENABLED = "automation-enabled";
 
     public static final List<String> NETBIOS_NAMES = Arrays.asList("PDBU-Hub3.0", "PowerView-Hub");
 
index adf16057a153ae2b0f4846479864276b53e0de75..5f7e42d2d308c7b8caf13c0167a73279a54ed5b3 100644 (file)
@@ -12,6 +12,8 @@
  */
 package org.openhab.binding.hdpowerview.internal;
 
+import java.util.Locale;
+
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.core.i18n.LocaleProvider;
@@ -44,4 +46,8 @@ public class HDPowerViewTranslationProvider {
         }
         return key;
     }
+
+    public Locale getLocale() {
+        return localeProvider.getLocale();
+    }
 }
index 0afa08478341a85ec5029fcc1f38359df0ac8889..8fee72166c96aaf6ae50d633f404cb846a1ea22e 100644 (file)
@@ -30,20 +30,23 @@ 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.ScheduledEvents;
 import org.openhab.binding.hdpowerview.internal.api.responses.Shade;
 import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.gson.Gson;
+import com.google.gson.JsonObject;
 import com.google.gson.JsonParseException;
+import com.google.gson.JsonParser;
 
 /**
  * JAX-RS targets for communicating with an HD PowerView hub
  *
  * @author Andy Lintner - Initial contribution
  * @author Andrew Fiddian-Green - Added support for secondary rail positions
- * @author Jacob Laursen - Add support for scene groups
+ * @author Jacob Laursen - Add support for scene groups and automations
  */
 @NonNullByDefault
 public class HDPowerViewWebTargets {
@@ -65,6 +68,7 @@ public class HDPowerViewWebTargets {
     private final String scenes;
     private final String sceneCollectionActivate;
     private final String sceneCollections;
+    private final String scheduledEvents;
 
     private final Gson gson = new Gson();
     private final HttpClient httpClient;
@@ -107,6 +111,7 @@ public class HDPowerViewWebTargets {
         scenes = base + "scenes/";
         sceneCollectionActivate = base + "sceneCollections";
         sceneCollections = base + "sceneCollections/";
+        scheduledEvents = base + "scheduledevents";
         this.httpClient = httpClient;
     }
 
@@ -189,6 +194,41 @@ public class HDPowerViewWebTargets {
                 Query.of("sceneCollectionId", Integer.toString(sceneCollectionId)), null);
     }
 
+    /**
+     * Fetches a JSON package that describes all scheduled events in the hub, and wraps it in
+     * a ScheduledEvents class instance
+     *
+     * @return ScheduledEvents 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 ScheduledEvents getScheduledEvents()
+            throws JsonParseException, HubProcessingException, HubMaintenanceException {
+        String json = invoke(HttpMethod.GET, scheduledEvents, null, null);
+        return gson.fromJson(json, ScheduledEvents.class);
+    }
+
+    /**
+     * Enables or disables a scheduled event in the hub.
+     * 
+     * @param scheduledEventId id of the scheduled event to be enabled or disabled
+     * @param enable true to enable scheduled event, false to disable
+     * @throws JsonParseException if there is a JSON parsing error
+     * @throws JsonSyntaxException if there is a JSON syntax error
+     * @throws HubProcessingException if there is any processing error
+     * @throws HubMaintenanceException if the hub is down for maintenance
+     */
+    public void enableScheduledEvent(int scheduledEventId, boolean enable)
+            throws JsonParseException, HubProcessingException, HubMaintenanceException {
+        String uri = scheduledEvents + "/" + scheduledEventId;
+        String json = invoke(HttpMethod.GET, uri, null, null);
+        JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject();
+        JsonObject scheduledEventObject = jsonObject.get("scheduledEvent").getAsJsonObject();
+        scheduledEventObject.addProperty("enabled", enable);
+        invoke(HttpMethod.PUT, uri, null, jsonObject.toString());
+    }
+
     /**
      * Invoke a call on the hub server to retrieve information or send a command
      *
index f4822b0693c7c28214c3257431ae90b1ef493e83..7a4aea8d5d318244bfdbaa1d0cb03991dd42ff2d 100644 (file)
@@ -20,7 +20,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 
 /**
- * State of all Scenes in an HD PowerView hub
+ * State of all Scene Collections in an HD PowerView hub
  *
  * @author Jacob Laursen - Initial contribution
  */
@@ -38,13 +38,45 @@ public class SceneCollections {
      */
     @SuppressWarnings("null")
     @NonNullByDefault
-    public static class SceneCollection {
+    public static class SceneCollection implements Comparable<SceneCollection> {
         public int id;
         public @Nullable String name;
         public int order;
         public int colorId;
         public int iconId;
 
+        @Override
+        public boolean equals(@Nullable Object o) {
+            if (o == this) {
+                return true;
+            }
+            if (!(o instanceof SceneCollection)) {
+                return false;
+            }
+            SceneCollection other = (SceneCollection) o;
+
+            return this.id == other.id && this.name.equals(other.name) && this.order == other.order
+                    && this.colorId == other.colorId && this.iconId == other.iconId;
+        }
+
+        @Override
+        public final int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + id;
+            result = prime * result + (name == null ? 0 : name.hashCode());
+            result = prime * result + order;
+            result = prime * result + colorId;
+            result = prime * result + iconId;
+
+            return result;
+        }
+
+        @Override
+        public int compareTo(SceneCollection other) {
+            return Integer.compare(order, other.order);
+        }
+
         public String getName() {
             return new String(Base64.getDecoder().decode(name), StandardCharsets.UTF_8);
         }
index cf759e84fc38908572bd37f3e4ad4be3bb54ab6c..c9372112a9a67909652f16ab7b790a0a8a7c63cc 100644 (file)
@@ -38,7 +38,7 @@ public class Scenes {
      */
     @SuppressWarnings("null")
     @NonNullByDefault
-    public static class Scene {
+    public static class Scene implements Comparable<Scene> {
         public int id;
         public @Nullable String name;
         public int roomId;
@@ -46,6 +46,39 @@ public class Scenes {
         public int colorId;
         public int iconId;
 
+        @Override
+        public boolean equals(@Nullable Object o) {
+            if (o == this) {
+                return true;
+            }
+            if (!(o instanceof Scene)) {
+                return false;
+            }
+            Scene other = (Scene) o;
+
+            return this.id == other.id && this.name.equals(other.name) && this.roomId == other.roomId
+                    && this.order == other.order && this.colorId == other.colorId && this.iconId == other.iconId;
+        }
+
+        @Override
+        public final int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + id;
+            result = prime * result + (name == null ? 0 : name.hashCode());
+            result = prime * result + roomId;
+            result = prime * result + order;
+            result = prime * result + colorId;
+            result = prime * result + iconId;
+
+            return result;
+        }
+
+        @Override
+        public int compareTo(Scene other) {
+            return Integer.compare(order, other.order);
+        }
+
         public String getName() {
             return new String(Base64.getDecoder().decode(name), StandardCharsets.UTF_8);
         }
diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/ScheduledEvents.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/ScheduledEvents.java
new file mode 100644 (file)
index 0000000..ec1fac3
--- /dev/null
@@ -0,0 +1,132 @@
+/**
+ * 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.time.DayOfWeek;
+import java.util.EnumSet;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * State of all Scheduled Events in an HD PowerView hub
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class ScheduledEvents {
+
+    public static final EnumSet<DayOfWeek> WEEKDAYS = EnumSet.of(DayOfWeek.MONDAY, DayOfWeek.TUESDAY,
+            DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY);
+
+    public static final EnumSet<DayOfWeek> WEEKENDS = EnumSet.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY);
+
+    public static final int SCHEDULED_EVENT_TYPE_TIME = 0;
+    public static final int SCHEDULED_EVENT_TYPE_SUNRISE = 1;
+    public static final int SCHEDULED_EVENT_TYPE_SUNSET = 2;
+
+    public @Nullable List<ScheduledEvent> scheduledEventData;
+    public @Nullable List<Integer> scheduledEventIds;
+
+    /*
+     * 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 ScheduledEvent {
+        public int id;
+        public boolean enabled;
+        public int sceneId;
+        public int sceneCollectionId;
+        public boolean daySunday;
+        public boolean dayMonday;
+        public boolean dayTuesday;
+        public boolean dayWednesday;
+        public boolean dayThursday;
+        public boolean dayFriday;
+        public boolean daySaturday;
+        public int eventType;
+        public int hour;
+        public int minute;
+
+        @Override
+        public boolean equals(@Nullable Object o) {
+            if (o == this) {
+                return true;
+            }
+            if (!(o instanceof ScheduledEvent)) {
+                return false;
+            }
+            ScheduledEvent other = (ScheduledEvent) o;
+
+            return this.id == other.id && this.enabled == other.enabled && this.sceneId == other.sceneId
+                    && this.sceneCollectionId == other.sceneCollectionId && this.daySunday == other.daySunday
+                    && this.dayMonday == other.dayMonday && this.dayTuesday == other.dayTuesday
+                    && this.dayWednesday == other.dayWednesday && this.dayThursday == other.dayThursday
+                    && this.dayFriday == other.dayFriday && this.daySaturday == other.daySaturday
+                    && this.eventType == other.eventType && this.hour == other.hour && this.minute == other.minute;
+        }
+
+        @Override
+        public final int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + id;
+            result = prime * result + (enabled ? 1 : 0);
+            result = prime * result + sceneId;
+            result = prime * result + sceneCollectionId;
+            result = prime * result + (daySunday ? 1 : 0);
+            result = prime * result + (dayMonday ? 1 : 0);
+            result = prime * result + (dayTuesday ? 1 : 0);
+            result = prime * result + (dayWednesday ? 1 : 0);
+            result = prime * result + (dayThursday ? 1 : 0);
+            result = prime * result + (dayFriday ? 1 : 0);
+            result = prime * result + (daySaturday ? 1 : 0);
+            result = prime * result + eventType;
+            result = prime * result + hour;
+            result = prime * result + minute;
+
+            return result;
+        }
+
+        public EnumSet<DayOfWeek> getDays() {
+            EnumSet<DayOfWeek> days = EnumSet.noneOf(DayOfWeek.class);
+            if (daySunday) {
+                days.add(DayOfWeek.SUNDAY);
+            }
+            if (dayMonday) {
+                days.add(DayOfWeek.MONDAY);
+            }
+            if (dayTuesday) {
+                days.add(DayOfWeek.TUESDAY);
+            }
+            if (dayWednesday) {
+                days.add(DayOfWeek.WEDNESDAY);
+            }
+            if (dayThursday) {
+                days.add(DayOfWeek.THURSDAY);
+            }
+            if (dayFriday) {
+                days.add(DayOfWeek.FRIDAY);
+            }
+            if (daySaturday) {
+                days.add(DayOfWeek.SATURDAY);
+            }
+            return days;
+        }
+    }
+}
index 2eda6b8b23598d2926dcb5bbf1692c3830050d4e..3b3579a363e7ff0c0d7a556abf09ba768420d998 100644 (file)
  */
 package org.openhab.binding.hdpowerview.internal.handler;
 
+import java.time.DayOfWeek;
+import java.time.LocalTime;
+import java.time.format.TextStyle;
 import java.util.ArrayList;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.StringJoiner;
+import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 
@@ -34,13 +40,17 @@ 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.ScheduledEvents;
+import org.openhab.binding.hdpowerview.internal.api.responses.ScheduledEvents.ScheduledEvent;
 import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
 import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData;
 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewHubConfiguration;
 import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration;
+import org.openhab.core.library.CoreItemFactory;
 import org.openhab.core.library.types.OnOffType;
 import org.openhab.core.thing.Bridge;
 import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelGroupUID;
 import org.openhab.core.thing.ChannelUID;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingStatus;
@@ -62,7 +72,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
+ * @author Jacob Laursen - Add support for scene groups and automations
  */
 @NonNullByDefault
 public class HDPowerViewHubHandler extends BaseBridgeHandler {
@@ -80,11 +90,19 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
     private @Nullable ScheduledFuture<?> hardRefreshPositionFuture;
     private @Nullable ScheduledFuture<?> hardRefreshBatteryLevelFuture;
 
+    private List<Scene> sceneCache = new CopyOnWriteArrayList<>();
+    private List<SceneCollection> sceneCollectionCache = new CopyOnWriteArrayList<>();
+    private List<ScheduledEvent> scheduledEventCache = new CopyOnWriteArrayList<>();
+    private Boolean deprecatedChannelsCreated = false;
+
     private final ChannelTypeUID sceneChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
             HDPowerViewBindingConstants.CHANNELTYPE_SCENE_ACTIVATE);
 
-    private final ChannelTypeUID sceneCollectionChannelTypeUID = new ChannelTypeUID(
-            HDPowerViewBindingConstants.BINDING_ID, HDPowerViewBindingConstants.CHANNELTYPE_SCENE_GROUP_ACTIVATE);
+    private final ChannelTypeUID sceneGroupChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
+            HDPowerViewBindingConstants.CHANNELTYPE_SCENE_GROUP_ACTIVATE);
+
+    private final ChannelTypeUID automationChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
+            HDPowerViewBindingConstants.CHANNELTYPE_AUTOMATION_ENABLED);
 
     public HDPowerViewHubHandler(Bridge bridge, HttpClient httpClient,
             HDPowerViewTranslationProvider translationProvider) {
@@ -100,10 +118,6 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
             return;
         }
 
-        if (!OnOffType.ON.equals(command)) {
-            return;
-        }
-
         Channel channel = getThing().getChannel(channelUID.getId());
         if (channel == null) {
             return;
@@ -114,11 +128,13 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
             if (webTargets == null) {
                 throw new ProcessingException("Web targets not initialized");
             }
-            int id = Integer.parseInt(channelUID.getId());
-            if (sceneChannelTypeUID.equals(channel.getChannelTypeUID())) {
+            int id = Integer.parseInt(channelUID.getIdWithoutGroup());
+            if (sceneChannelTypeUID.equals(channel.getChannelTypeUID()) && OnOffType.ON.equals(command)) {
                 webTargets.activateScene(id);
-            } else if (sceneCollectionChannelTypeUID.equals(channel.getChannelTypeUID())) {
+            } else if (sceneGroupChannelTypeUID.equals(channel.getChannelTypeUID()) && OnOffType.ON.equals(command)) {
                 webTargets.activateSceneCollection(id);
+            } else if (automationChannelTypeUID.equals(channel.getChannelTypeUID())) {
+                webTargets.enableScheduledEvent(id, OnOffType.ON.equals(command));
             }
         } catch (HubMaintenanceException e) {
             // exceptions are logged in HDPowerViewWebTargets
@@ -143,9 +159,18 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
         refreshInterval = config.refresh;
         hardRefreshPositionInterval = config.hardRefresh;
         hardRefreshBatteryLevelInterval = config.hardRefreshBatteryLevel;
+        initializeChannels();
         schedulePoll();
     }
 
+    private void initializeChannels() {
+        // Rebuild dynamic channels and synchronize with cache.
+        updateThing(editThing().withChannels(new ArrayList<Channel>()).build());
+        sceneCache.clear();
+        sceneCollectionCache.clear();
+        scheduledEventCache.clear();
+    }
+
     public @Nullable HDPowerViewWebTargets getWebTargets() {
         return webTargets;
     }
@@ -215,8 +240,14 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
         try {
             logger.debug("Polling for state");
             pollShades();
-            pollScenes();
-            pollSceneCollections();
+
+            List<Scene> scenes = updateSceneChannels();
+            List<SceneCollection> sceneCollections = updateSceneCollectionChannels();
+            List<ScheduledEvent> scheduledEvents = updateScheduledEventChannels(scenes, sceneCollections);
+
+            // Scheduled events should also have their current state updated if event has been
+            // enabled or disabled through app or other integration.
+            updateScheduledEventStates(scheduledEvents);
         } catch (JsonParseException e) {
             logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
         } catch (HubProcessingException e) {
@@ -270,7 +301,7 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
         thingHandler.onReceiveUpdate(shadeData);
     }
 
-    private void pollScenes() throws JsonParseException, HubProcessingException, HubMaintenanceException {
+    private List<Scene> fetchScenes() throws JsonParseException, HubProcessingException, HubMaintenanceException {
         HDPowerViewWebTargets webTargets = this.webTargets;
         if (webTargets == null) {
             throw new ProcessingException("Web targets not initialized");
@@ -287,41 +318,86 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
         }
         logger.debug("Received data for {} scenes", sceneData.size());
 
-        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);
-            if (idChannelMap.containsKey(sceneId)) {
-                idChannelMap.remove(sceneId);
-                logger.debug("Keeping channel for existing scene '{}'", sceneId);
-            } 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(description).build();
-                allChannels.add(channel);
-                isChannelListChanged = true;
-                logger.debug("Creating new channel for scene '{}'", sceneId);
-            }
-        }
+        return sceneData;
+    }
+
+    private List<Scene> updateSceneChannels()
+            throws JsonParseException, HubProcessingException, HubMaintenanceException {
+        List<Scene> scenes = fetchScenes();
 
-        // remove any previously created channels that no longer exist
-        if (!idChannelMap.isEmpty()) {
-            logger.debug("Removing {} orphan scene channels", idChannelMap.size());
-            allChannels.removeAll(idChannelMap.values());
-            isChannelListChanged = true;
+        if (scenes.size() == sceneCache.size() && sceneCache.containsAll(scenes)) {
+            // Duplicates are not allowed. Reordering is not supported.
+            logger.debug("Preserving scene channels, no changes detected");
+            return scenes;
         }
 
-        if (isChannelListChanged) {
-            updateThing(editThing().withChannels(allChannels).build());
+        logger.debug("Updating all scene channels, changes detected");
+        sceneCache = new CopyOnWriteArrayList<Scene>(scenes);
+
+        List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
+        allChannels.removeIf(c -> HDPowerViewBindingConstants.CHANNEL_GROUP_SCENES.equals(c.getUID().getGroupId()));
+        scenes.stream().sorted().forEach(scene -> allChannels.add(createSceneChannel(scene)));
+        updateThing(editThing().withChannels(allChannels).build());
+
+        createDeprecatedSceneChannels(scenes);
+
+        return scenes;
+    }
+
+    private Channel createSceneChannel(Scene scene) {
+        ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
+                HDPowerViewBindingConstants.CHANNEL_GROUP_SCENES);
+        ChannelUID channelUid = new ChannelUID(channelGroupUid, Integer.toString(scene.id));
+        String description = translationProvider.getText("dynamic-channel.scene-activate.description", scene.getName());
+        Channel channel = ChannelBuilder.create(channelUid, CoreItemFactory.SWITCH).withType(sceneChannelTypeUID)
+                .withLabel(scene.getName()).withDescription(description).build();
+
+        return channel;
+    }
+
+    /**
+     * Create backwards compatible scene channels if any items configured before release 3.2
+     * are still linked. Users should have a reasonable amount of time to migrate to the new
+     * scene channels that are connected to a channel group.
+     */
+    private void createDeprecatedSceneChannels(List<Scene> scenes) {
+        if (deprecatedChannelsCreated) {
+            // Only do this once.
+            return;
         }
+        ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
+                HDPowerViewBindingConstants.CHANNEL_GROUP_SCENES);
+        for (Scene scene : scenes) {
+            String channelId = Integer.toString(scene.id);
+            ChannelUID newChannelUid = new ChannelUID(channelGroupUid, channelId);
+            ChannelUID deprecatedChannelUid = new ChannelUID(getThing().getUID(), channelId);
+            String description = translationProvider.getText("dynamic-channel.scene-activate.deprecated.description",
+                    scene.getName());
+            Channel channel = ChannelBuilder.create(deprecatedChannelUid, CoreItemFactory.SWITCH)
+                    .withType(sceneChannelTypeUID).withLabel(scene.getName()).withDescription(description).build();
+            logger.debug("Creating deprecated channel '{}' ('{}') to probe for linked items", deprecatedChannelUid,
+                    scene.getName());
+            updateThing(editThing().withChannel(channel).build());
+            if (this.isLinked(deprecatedChannelUid) && !this.isLinked(newChannelUid)) {
+                logger.warn("Created deprecated channel '{}' ('{}'), please link items to '{}' instead",
+                        deprecatedChannelUid, scene.getName(), newChannelUid);
+            } else {
+                if (this.isLinked(newChannelUid)) {
+                    logger.debug("Removing deprecated channel '{}' ('{}') since new channel '{}' is linked",
+                            deprecatedChannelUid, scene.getName(), newChannelUid);
+
+                } else {
+                    logger.debug("Removing deprecated channel '{}' ('{}') since it has no linked items",
+                            deprecatedChannelUid, scene.getName());
+                }
+                updateThing(editThing().withoutChannel(deprecatedChannelUid).build());
+            }
+        }
+        deprecatedChannelsCreated = true;
     }
 
-    private void pollSceneCollections() throws JsonParseException, HubProcessingException, HubMaintenanceException {
+    private List<SceneCollection> fetchSceneCollections()
+            throws JsonParseException, HubProcessingException, HubMaintenanceException {
         HDPowerViewWebTargets webTargets = this.webTargets;
         if (webTargets == null) {
             throw new ProcessingException("Web targets not initialized");
@@ -338,37 +414,206 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
         }
         logger.debug("Received data for {} sceneCollections", sceneCollectionData.size());
 
-        Map<String, Channel> idChannelMap = getIdSceneCollectionChannelMap();
+        return sceneCollectionData;
+    }
+
+    private List<SceneCollection> updateSceneCollectionChannels()
+            throws JsonParseException, HubProcessingException, HubMaintenanceException {
+        List<SceneCollection> sceneCollections = fetchSceneCollections();
+
+        if (sceneCollections.size() == sceneCollectionCache.size()
+                && sceneCollectionCache.containsAll(sceneCollections)) {
+            // Duplicates are not allowed. Reordering is not supported.
+            logger.debug("Preserving scene collection channels, no changes detected");
+            return sceneCollections;
+        }
+
+        logger.debug("Updating all scene collection channels, changes detected");
+        sceneCollectionCache = new CopyOnWriteArrayList<SceneCollection>(sceneCollections);
+
         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
+                .removeIf(c -> HDPowerViewBindingConstants.CHANNEL_GROUP_SCENE_GROUPS.equals(c.getUID().getGroupId()));
+        sceneCollections.stream().sorted()
+                .forEach(sceneCollection -> allChannels.add(createSceneCollectionChannel(sceneCollection)));
+        updateThing(editThing().withChannels(allChannels).build());
+
+        return sceneCollections;
+    }
+
+    private Channel createSceneCollectionChannel(SceneCollection sceneCollection) {
+        ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
+                HDPowerViewBindingConstants.CHANNEL_GROUP_SCENE_GROUPS);
+        ChannelUID channelUid = new ChannelUID(channelGroupUid, Integer.toString(sceneCollection.id));
+        String description = translationProvider.getText("dynamic-channel.scene-group-activate.description",
+                sceneCollection.getName());
+        Channel channel = ChannelBuilder.create(channelUid, CoreItemFactory.SWITCH).withType(sceneGroupChannelTypeUID)
+                .withLabel(sceneCollection.getName()).withDescription(description).build();
+
+        return channel;
+    }
+
+    private List<ScheduledEvent> fetchScheduledEvents()
+            throws JsonParseException, HubProcessingException, HubMaintenanceException {
+        HDPowerViewWebTargets webTargets = this.webTargets;
+        if (webTargets == null) {
+            throw new ProcessingException("Web targets not initialized");
+        }
+
+        ScheduledEvents scheduledEvents = webTargets.getScheduledEvents();
+        if (scheduledEvents == null) {
+            throw new JsonParseException("Missing 'scheduledEvents' element");
+        }
+
+        List<ScheduledEvent> scheduledEventData = scheduledEvents.scheduledEventData;
+        if (scheduledEventData == null) {
+            throw new JsonParseException("Missing 'scheduledEvents.scheduledEventData' element");
+        }
+        logger.debug("Received data for {} scheduledEvents", scheduledEventData.size());
+
+        return scheduledEventData;
+    }
+
+    private List<ScheduledEvent> updateScheduledEventChannels(List<Scene> scenes,
+            List<SceneCollection> sceneCollections)
+            throws JsonParseException, HubProcessingException, HubMaintenanceException {
+        List<ScheduledEvent> scheduledEvents = fetchScheduledEvents();
+
+        if (scheduledEvents.size() == scheduledEventCache.size() && scheduledEventCache.containsAll(scheduledEvents)) {
+            // Duplicates are not allowed. Reordering is not supported.
+            logger.debug("Preserving scheduled event channels, no changes detected");
+            return scheduledEvents;
+        }
+
+        logger.debug("Updating all scheduled event channels, changes detected");
+        scheduledEventCache = new CopyOnWriteArrayList<ScheduledEvent>(scheduledEvents);
+
+        List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
+        allChannels
+                .removeIf(c -> HDPowerViewBindingConstants.CHANNEL_GROUP_AUTOMATIONS.equals(c.getUID().getGroupId()));
+        scheduledEvents.stream().forEach(scheduledEvent -> {
+            Channel channel = createScheduledEventChannel(scheduledEvent, scenes, sceneCollections);
+            if (channel != null) {
                 allChannels.add(channel);
-                isChannelListChanged = true;
-                logger.debug("Creating new channel for scene collection '{}'", sceneCollectionId);
             }
+        });
+        updateThing(editThing().withChannels(allChannels).build());
+
+        return scheduledEvents;
+    }
+
+    private @Nullable Channel createScheduledEventChannel(ScheduledEvent scheduledEvent, List<Scene> scenes,
+            List<SceneCollection> sceneCollections) {
+        String referencedName = getReferencedSceneOrSceneCollectionName(scheduledEvent, scenes, sceneCollections);
+        if (referencedName == null) {
+            return null;
+        }
+        ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
+                HDPowerViewBindingConstants.CHANNEL_GROUP_AUTOMATIONS);
+        ChannelUID channelUid = new ChannelUID(channelGroupUid, Integer.toString(scheduledEvent.id));
+        String label = getScheduledEventName(referencedName, scheduledEvent);
+        String description = translationProvider.getText("dynamic-channel.automation-enabled.description",
+                referencedName);
+        Channel channel = ChannelBuilder.create(channelUid, CoreItemFactory.SWITCH).withType(automationChannelTypeUID)
+                .withLabel(label).withDescription(description).build();
+
+        return channel;
+    }
+
+    private @Nullable String getReferencedSceneOrSceneCollectionName(ScheduledEvent scheduledEvent, List<Scene> scenes,
+            List<SceneCollection> sceneCollections) {
+        if (scheduledEvent.sceneId > 0) {
+            for (Scene scene : scenes) {
+                if (scene.id == scheduledEvent.sceneId) {
+                    return scene.getName();
+                }
+            }
+            logger.error("Scene '{}' was not found for scheduled event '{}'", scheduledEvent.sceneId,
+                    scheduledEvent.id);
+            return null;
+        } else if (scheduledEvent.sceneCollectionId > 0) {
+            for (SceneCollection sceneCollection : sceneCollections) {
+                if (sceneCollection.id == scheduledEvent.sceneCollectionId) {
+                    return sceneCollection.getName();
+                }
+            }
+            logger.error("Scene collection '{}' was not found for scheduled event '{}'",
+                    scheduledEvent.sceneCollectionId, scheduledEvent.id);
+            return null;
+        } else {
+            logger.error("Scheduled event '{}'' not related to any scene or scene collection", scheduledEvent.id);
+            return null;
         }
+    }
 
-        // 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;
+    private String getScheduledEventName(String sceneName, ScheduledEvent scheduledEvent) {
+        String timeString, daysString;
+
+        switch (scheduledEvent.eventType) {
+            case ScheduledEvents.SCHEDULED_EVENT_TYPE_TIME:
+                timeString = LocalTime.of(scheduledEvent.hour, scheduledEvent.minute).toString();
+                break;
+            case ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNRISE:
+                if (scheduledEvent.minute == 0) {
+                    timeString = translationProvider.getText("dynamic-channel.automation.at_sunrise");
+                } else if (scheduledEvent.minute < 0) {
+                    timeString = translationProvider.getText("dynamic-channel.automation.before_sunrise",
+                            getFormattedTimeOffset(-scheduledEvent.minute));
+                } else {
+                    timeString = translationProvider.getText("dynamic-channel.automation.after_sunrise",
+                            getFormattedTimeOffset(scheduledEvent.minute));
+                }
+                break;
+            case ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNSET:
+                if (scheduledEvent.minute == 0) {
+                    timeString = translationProvider.getText("dynamic-channel.automation.at_sunset");
+                } else if (scheduledEvent.minute < 0) {
+                    timeString = translationProvider.getText("dynamic-channel.automation.before_sunset",
+                            getFormattedTimeOffset(-scheduledEvent.minute));
+                } else {
+                    timeString = translationProvider.getText("dynamic-channel.automation.after_sunset",
+                            getFormattedTimeOffset(scheduledEvent.minute));
+                }
+                break;
+            default:
+                return sceneName;
+        }
+
+        EnumSet<DayOfWeek> days = scheduledEvent.getDays();
+        if (EnumSet.allOf(DayOfWeek.class).equals(days)) {
+            daysString = translationProvider.getText("dynamic-channel.automation.all-days");
+        } else if (ScheduledEvents.WEEKDAYS.equals(days)) {
+            daysString = translationProvider.getText("dynamic-channel.automation.weekdays");
+        } else if (ScheduledEvents.WEEKENDS.equals(days)) {
+            daysString = translationProvider.getText("dynamic-channel.automation.weekends");
+        } else {
+            StringJoiner joiner = new StringJoiner(", ");
+            days.forEach(day -> joiner.add(day.getDisplayName(TextStyle.SHORT, translationProvider.getLocale())));
+            daysString = joiner.toString();
         }
 
-        if (isChannelListChanged) {
-            updateThing(editThing().withChannels(allChannels).build());
+        return translationProvider.getText("dynamic-channel.automation-enabled.label", sceneName, timeString,
+                daysString);
+    }
+
+    private String getFormattedTimeOffset(int minutes) {
+        if (minutes >= 60) {
+            int remainder = minutes % 60;
+            if (remainder == 0) {
+                return translationProvider.getText("dynamic-channel.automation.hour", minutes / 60);
+            }
+            return translationProvider.getText("dynamic-channel.automation.hour-minute", minutes / 60, remainder);
+        }
+        return translationProvider.getText("dynamic-channel.automation.minute", minutes);
+    }
+
+    private void updateScheduledEventStates(List<ScheduledEvent> scheduledEvents) {
+        ChannelGroupUID channelGroupUid = new ChannelGroupUID(thing.getUID(),
+                HDPowerViewBindingConstants.CHANNEL_GROUP_AUTOMATIONS);
+        for (ScheduledEvent scheduledEvent : scheduledEvents) {
+            String scheduledEventId = Integer.toString(scheduledEvent.id);
+            ChannelUID channelUid = new ChannelUID(channelGroupUid, scheduledEventId);
+            updateState(channelUid, scheduledEvent.enabled ? OnOffType.ON : OnOffType.OFF);
         }
     }
 
@@ -393,26 +638,6 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
         return ret;
     }
 
-    private Map<String, Channel> getIdSceneChannelMap() {
-        Map<String, Channel> ret = new HashMap<>();
-        for (Channel channel : getThing().getChannels()) {
-            if (sceneChannelTypeUID.equals(channel.getChannelTypeUID())) {
-                ret.put(channel.getUID().getId(), channel);
-            }
-        }
-        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()) {
index 9e8e94cd181794a88249d3da6e573fdfb4d74bbf..8c0876b6366dd93173ddef25e44e2e0ae03a642f 100644 (file)
@@ -43,4 +43,19 @@ offline.conf-error.invalid-bridge-handler = Invalid bridge handler
 # dynamic channels
 
 dynamic-channel.scene-activate.description = Activates the scene ''{0}''
+dynamic-channel.scene-activate.deprecated.description = DEPRECATED: Activates the scene ''{0}''
 dynamic-channel.scene-group-activate.description = Activates the scene group ''{0}''
+dynamic-channel.automation-enabled.description = Enables/disables the automation ''{0}''
+dynamic-channel.automation-enabled.label = {0}, {1}, {2}
+dynamic-channel.automation.hour = {0}hr
+dynamic-channel.automation.minute = {0}m
+dynamic-channel.automation.hour-minute = {0}hr {1}m
+dynamic-channel.automation.at_sunrise = At sunrise
+dynamic-channel.automation.before_sunrise = {0} before sunrise
+dynamic-channel.automation.after_sunrise = {0} after sunrise
+dynamic-channel.automation.at_sunset = At sunset
+dynamic-channel.automation.before_sunset = {0} before sunset
+dynamic-channel.automation.after_sunset = {0} after sunset
+dynamic-channel.automation.weekdays = Weekdays
+dynamic-channel.automation.weekends = Weekends
+dynamic-channel.automation.all-days = All days
diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/i18n/hdpowerview_da.properties b/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/i18n/hdpowerview_da.properties
new file mode 100644 (file)
index 0000000..6b4483f
--- /dev/null
@@ -0,0 +1,19 @@
+# dynamic channels
+
+dynamic-channel.scene-activate.description = Aktiverer scenen ''{0}''
+dynamic-channel.scene-activate.deprecated.description = UDFASET: Aktiverer scenen ''{0}''
+dynamic-channel.scene-group-activate.description = Aktiverer scenegruppen ''{0}''
+dynamic-channel.automation-enabled.description = Aktiverer/deaktiverer automatiseringen ''{0}''
+dynamic-channel.automation-enabled.label = {0}, {1}, {2}
+dynamic-channel.automation.hour = {0}t
+dynamic-channel.automation.minute = {0}m
+dynamic-channel.automation.hour-minute = {0}t {1}m
+dynamic-channel.automation.at_sunrise = Ved solopgang
+dynamic-channel.automation.before_sunrise = {0} før solopgang
+dynamic-channel.automation.after_sunrise = {0} efter solopgang
+dynamic-channel.automation.at_sunset = Ved solnedgang
+dynamic-channel.automation.before_sunset = {0} før solnedgang
+dynamic-channel.automation.after_sunset = {0} efter solnedgang
+dynamic-channel.automation.weekdays = Ugedage
+dynamic-channel.automation.weekends = Weekend
+dynamic-channel.automation.all-days = Alle dage
index 22954d4097f202f4999d40020f95f5fe739ddd33..6769c07c9a00554f8b5e8be2cafff5e1696ae582 100644 (file)
@@ -8,6 +8,12 @@
                <label>PowerView Hub</label>
                <description>Hunter Douglas (Luxaflex) PowerView Hub</description>
 
+               <channel-groups>
+                       <channel-group id="scenes" typeId="scenes"/>
+                       <channel-group id="sceneGroups" typeId="sceneGroups"/>
+                       <channel-group id="automations" typeId="automations"/>
+               </channel-groups>
+
                <properties>
                        <property name="vendor">Hunter Douglas (Luxaflex)</property>
                        <property name="modelId">PowerView Hub</property>
                <label>Activate</label>
        </channel-type>
 
+       <channel-type id="automation-enabled">
+               <item-type>Switch</item-type>
+               <label>Enable</label>
+       </channel-type>
+
        <channel-type id="battery-voltage" advanced="true">
                <item-type>Number:ElectricPotential</item-type>
                <label>Battery Voltage</label>
                <state pattern="%.1f %unit%" readOnly="true"/>
        </channel-type>
 
+       <channel-group-type id="scenes">
+               <label>Scenes</label>
+       </channel-group-type>
+
+       <channel-group-type id="sceneGroups">
+               <label>Scene Groups</label>
+       </channel-group-type>
+
+       <channel-group-type id="automations">
+               <label>Automations</label>
+       </channel-group-type>
+
 </thing:thing-descriptions>