[hdpowerview] Add support for enabling/disabling automations (#11637)

* 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>
This commit is contained in:
jlaur 2021-12-11 17:20:11 +01:00 committed by GitHub
parent 145bd0ec97
commit 9f339c8ec4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 633 additions and 101 deletions

View 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

View 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");

View 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();
}
}

View 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
*

View 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);
}

View 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);
}

View File

@ -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;
}
}
}

View File

@ -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<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);
}
}
// 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<Scene> updateSceneChannels()
throws JsonParseException, HubProcessingException, HubMaintenanceException {
List<Scene> 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<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 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;
}
}
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<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()) {

View 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

View File

@ -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

View 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>
@ -96,6 +102,11 @@
<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>
@ -103,4 +114,16 @@
<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>