diff --git a/bundles/org.openhab.binding.hdpowerview/README.md b/bundles/org.openhab.binding.hdpowerview/README.md index 60601cf6c..023e62139 100644 --- a/bundles/org.openhab.binding.hdpowerview/README.md +++ b/bundles/org.openhab.binding.hdpowerview/README.md @@ -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" (g_Shades_Scene_Trigger) {channel="hdpowerview:hub:g24:22663", autoupdate="false"} +Switch Living_Room_Shades_Scene_Heart "Living Room Shades Scene Heart" (g_Shades_Scene_Trigger) {channel="hdpowerview:hub:g24:scenes#22663", autoupdate="false"} ``` ### `demo.sitemap` File diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewBindingConstants.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewBindingConstants.java index 6eb621157..e4352e6a7 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewBindingConstants.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewBindingConstants.java @@ -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 NETBIOS_NAMES = Arrays.asList("PDBU-Hub3.0", "PowerView-Hub"); 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 index adf16057a..5f7e42d2d 100644 --- 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 @@ -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(); + } } diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java index 0afa08478..8fee72166 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java @@ -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 * 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 index f4822b069..7a4aea8d5 100644 --- 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 @@ -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 { 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); } diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Scenes.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Scenes.java index cf759e84f..c9372112a 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Scenes.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Scenes.java @@ -38,7 +38,7 @@ public class Scenes { */ @SuppressWarnings("null") @NonNullByDefault - public static class Scene { + public static class Scene implements Comparable { 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 index 000000000..ec1fac312 --- /dev/null +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/ScheduledEvents.java @@ -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 WEEKDAYS = EnumSet.of(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY); + + public static final EnumSet 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 scheduledEventData; + public @Nullable List 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 getDays() { + EnumSet 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; + } + } +} diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java index 2eda6b8b2..3b3579a36 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java @@ -12,11 +12,17 @@ */ 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 sceneCache = new CopyOnWriteArrayList<>(); + private List sceneCollectionCache = new CopyOnWriteArrayList<>(); + private List 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()).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 scenes = updateSceneChannels(); + List sceneCollections = updateSceneCollectionChannels(); + List 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 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 idChannelMap = getIdSceneChannelMap(); - List 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); - } - } - - // 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 (isChannelListChanged) { - updateThing(editThing().withChannels(allChannels).build()); - } + return sceneData; } - private void pollSceneCollections() throws JsonParseException, HubProcessingException, HubMaintenanceException { + private List updateSceneChannels() + throws JsonParseException, HubProcessingException, HubMaintenanceException { + List scenes = fetchScenes(); + + 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; + } + + logger.debug("Updating all scene channels, changes detected"); + sceneCache = new CopyOnWriteArrayList(scenes); + + List 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 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 List 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 idChannelMap = getIdSceneCollectionChannelMap(); + return sceneCollectionData; + } + + private List updateSceneCollectionChannels() + throws JsonParseException, HubProcessingException, HubMaintenanceException { + List 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(sceneCollections); + List 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 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 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 updateScheduledEventChannels(List scenes, + List sceneCollections) + throws JsonParseException, HubProcessingException, HubMaintenanceException { + List 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(scheduledEvents); + + List 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 scenes, + List 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 scenes, + List 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; + } + } + + 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; } - // 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; + EnumSet 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 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 getIdSceneChannelMap() { - Map ret = new HashMap<>(); - for (Channel channel : getThing().getChannels()) { - if (sceneChannelTypeUID.equals(channel.getChannelTypeUID())) { - ret.put(channel.getUID().getId(), channel); - } - } - return ret; - } - - private Map getIdSceneCollectionChannelMap() { - Map 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 thingIdMap = getThingIdMap(); for (Entry 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 index 9e8e94cd1..8c0876b63 100644 --- 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 @@ -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 index 000000000..6b4483f26 --- /dev/null +++ b/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/i18n/hdpowerview_da.properties @@ -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 diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/thing/thing-types.xml index 22954d409..6769c07c9 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/thing/thing-types.xml @@ -8,6 +8,12 @@ Hunter Douglas (Luxaflex) PowerView Hub + + + + + + Hunter Douglas (Luxaflex) PowerView Hub @@ -96,6 +102,11 @@ + + Switch + + + Number:ElectricPotential @@ -103,4 +114,16 @@ + + + + + + + + + + + +