[hdpowerview] Add support for scene groups (#11534)

* Add support for scene collections.

Fixes #11533

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Add unit test for parsing of scene collections response.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Add default i18n properties file.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Fix CAT: File does not end with a newline.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Update documentation with scene collections.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Fix CAT: File does not end with a newline.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Fix formatting.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Fix CAT: File does not end with a newline.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Split offline tests into separate distinct tests.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Increase test coverage for scene/scene collection parsing.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Internationalization of dynamic scene/scene collection channels.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Rename scene collections to scene groups.

Renamed for all user-oriented texts/references to be consistent with now abandoned feature of the PowerView app.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Change custom text keys to not collide with framework.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Avoid multiple thing updates.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Add missing label/description texts for secondary channel.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>

* Remove unneeded @Nullable annotations.

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
This commit is contained in:
jlaur 2021-11-15 23:53:23 +01:00 committed by GitHub
parent 2973f6d890
commit 8c83c27c57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 420 additions and 96 deletions

View File

@ -60,12 +60,13 @@ However, the configuration parameters are described below:
### Channels for PowerView Hub ### Channels for PowerView Hub
Scene channels will be added dynamically to the binding as they are discovered in the hub. Scene and scene group channels will be added dynamically to the binding as they are discovered in the hub.
Each scene channel will have an entry in the hub as shown below, whereby different scenes have different `id` values: Each scene/scene group channel will have an entry in the hub as shown below, whereby different scenes/scene groups
have different `id` values:
| Channel | Item Type | Description | | Channel | Item Type | Description |
|----------|-----------| ------------| |----------|-----------| ------------|
| id | Switch | Turning this to ON will activate the scene. Scenes are stateless in the PowerView hub; they have no on/off state. Note: include `{autoupdate="false"}` in the item configuration to avoid having to reset it to off after use. | | id | Switch | Turning this to ON will activate the scene/scene group. Scenes/scene groups are stateless in the PowerView hub; they have no on/off state. Note: include `{autoupdate="false"}` in the item configuration to avoid having to reset it to off after use. |
### Channels for PowerView Shade ### Channels for PowerView Shade

View File

@ -26,6 +26,7 @@ import org.openhab.core.thing.ThingTypeUID;
* *
* @author Andy Lintner - Initial contribution * @author Andy Lintner - Initial contribution
* @author Andrew Fiddian-Green - Added support for secondary rail positions * @author Andrew Fiddian-Green - Added support for secondary rail positions
* @author Jacob Laursen - Add support for scene groups
*/ */
@NonNullByDefault @NonNullByDefault
public class HDPowerViewBindingConstants { public class HDPowerViewBindingConstants {
@ -46,6 +47,7 @@ public class HDPowerViewBindingConstants {
public static final String CHANNEL_SHADE_SIGNAL_STRENGTH = "signalStrength"; public static final String CHANNEL_SHADE_SIGNAL_STRENGTH = "signalStrength";
public static final String CHANNELTYPE_SCENE_ACTIVATE = "scene-activate"; public static final String CHANNELTYPE_SCENE_ACTIVATE = "scene-activate";
public static final String CHANNELTYPE_SCENE_GROUP_ACTIVATE = "scene-group-activate";
public static final List<String> NETBIOS_NAMES = Arrays.asList("PDBU-Hub3.0", "PowerView-Hub"); public static final List<String> NETBIOS_NAMES = Arrays.asList("PDBU-Hub3.0", "PowerView-Hub");

View File

@ -21,6 +21,8 @@ import org.openhab.binding.hdpowerview.internal.discovery.HDPowerViewShadeDiscov
import org.openhab.binding.hdpowerview.internal.handler.HDPowerViewHubHandler; import org.openhab.binding.hdpowerview.internal.handler.HDPowerViewHubHandler;
import org.openhab.binding.hdpowerview.internal.handler.HDPowerViewShadeHandler; import org.openhab.binding.hdpowerview.internal.handler.HDPowerViewShadeHandler;
import org.openhab.core.config.discovery.DiscoveryService; import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
@ -28,6 +30,7 @@ import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory; import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory; import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.Reference;
@ -43,10 +46,16 @@ import org.osgi.service.component.annotations.Reference;
public class HDPowerViewHandlerFactory extends BaseThingHandlerFactory { public class HDPowerViewHandlerFactory extends BaseThingHandlerFactory {
private final HttpClient httpClient; private final HttpClient httpClient;
private final HDPowerViewTranslationProvider translationProvider;
@Activate @Activate
public HDPowerViewHandlerFactory(@Reference HttpClientFactory httpClientFactory) { public HDPowerViewHandlerFactory(@Reference HttpClientFactory httpClientFactory,
final @Reference TranslationProvider i18nProvider, final @Reference LocaleProvider localeProvider,
ComponentContext componentContext) {
super.activate(componentContext);
this.httpClient = httpClientFactory.getCommonHttpClient(); this.httpClient = httpClientFactory.getCommonHttpClient();
this.translationProvider = new HDPowerViewTranslationProvider(getBundleContext().getBundle(), i18nProvider,
localeProvider);
} }
@Override @Override
@ -59,7 +68,7 @@ public class HDPowerViewHandlerFactory extends BaseThingHandlerFactory {
ThingTypeUID thingTypeUID = thing.getThingTypeUID(); ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(HDPowerViewBindingConstants.THING_TYPE_HUB)) { if (thingTypeUID.equals(HDPowerViewBindingConstants.THING_TYPE_HUB)) {
HDPowerViewHubHandler handler = new HDPowerViewHubHandler((Bridge) thing, httpClient); HDPowerViewHubHandler handler = new HDPowerViewHubHandler((Bridge) thing, httpClient, translationProvider);
registerService(new HDPowerViewShadeDiscoveryService(handler)); registerService(new HDPowerViewShadeDiscoveryService(handler));
return handler; return handler;
} else if (thingTypeUID.equals(HDPowerViewBindingConstants.THING_TYPE_SHADE)) { } else if (thingTypeUID.equals(HDPowerViewBindingConstants.THING_TYPE_SHADE)) {

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hdpowerview.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.osgi.framework.Bundle;
/**
* {@link HDPowerViewTranslationProvider} provides i18n message lookup
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class HDPowerViewTranslationProvider {
private final Bundle bundle;
private final TranslationProvider i18nProvider;
private final LocaleProvider localeProvider;
public HDPowerViewTranslationProvider(Bundle bundle, TranslationProvider i18nProvider,
LocaleProvider localeProvider) {
this.bundle = bundle;
this.i18nProvider = i18nProvider;
this.localeProvider = localeProvider;
}
public String getText(String key, @Nullable Object... arguments) {
String text = i18nProvider.getText(bundle, key, key, localeProvider.getLocale(), arguments);
if (text != null) {
return text;
}
return key;
}
}

View File

@ -28,6 +28,7 @@ import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.hdpowerview.internal.api.ShadePosition; import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
import org.openhab.binding.hdpowerview.internal.api.requests.ShadeMove; 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.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.Scenes;
import org.openhab.binding.hdpowerview.internal.api.responses.Shade; import org.openhab.binding.hdpowerview.internal.api.responses.Shade;
import org.openhab.binding.hdpowerview.internal.api.responses.Shades; import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
@ -42,6 +43,7 @@ import com.google.gson.JsonParseException;
* *
* @author Andy Lintner - Initial contribution * @author Andy Lintner - Initial contribution
* @author Andrew Fiddian-Green - Added support for secondary rail positions * @author Andrew Fiddian-Green - Added support for secondary rail positions
* @author Jacob Laursen - Add support for scene groups
*/ */
@NonNullByDefault @NonNullByDefault
public class HDPowerViewWebTargets { public class HDPowerViewWebTargets {
@ -61,6 +63,8 @@ public class HDPowerViewWebTargets {
private final String shades; private final String shades;
private final String sceneActivate; private final String sceneActivate;
private final String scenes; private final String scenes;
private final String sceneCollectionActivate;
private final String sceneCollections;
private final Gson gson = new Gson(); private final Gson gson = new Gson();
private final HttpClient httpClient; private final HttpClient httpClient;
@ -101,6 +105,8 @@ public class HDPowerViewWebTargets {
shades = base + "shades/"; shades = base + "shades/";
sceneActivate = base + "scenes"; sceneActivate = base + "scenes";
scenes = base + "scenes/"; scenes = base + "scenes/";
sceneCollectionActivate = base + "sceneCollections";
sceneCollections = base + "sceneCollections/";
this.httpClient = httpClient; this.httpClient = httpClient;
} }
@ -156,6 +162,33 @@ public class HDPowerViewWebTargets {
invoke(HttpMethod.GET, sceneActivate, Query.of("sceneId", Integer.toString(sceneId)), null); invoke(HttpMethod.GET, sceneActivate, Query.of("sceneId", Integer.toString(sceneId)), null);
} }
/**
* Fetches a JSON package that describes all scene collections in the hub, and wraps it in
* a SceneCollections class instance
*
* @return SceneCollections class instance
* @throws JsonParseException if there is a JSON parsing error
* @throws HubProcessingException if there is any processing error
* @throws HubMaintenanceException if the hub is down for maintenance
*/
public @Nullable SceneCollections getSceneCollections()
throws JsonParseException, HubProcessingException, HubMaintenanceException {
String json = invoke(HttpMethod.GET, sceneCollections, null, null);
return gson.fromJson(json, SceneCollections.class);
}
/**
* Instructs the hub to execute a specific scene collection
*
* @param sceneCollectionId id of the scene collection to be executed
* @throws HubProcessingException if there is any processing error
* @throws HubMaintenanceException if the hub is down for maintenance
*/
public void activateSceneCollection(int sceneCollectionId) throws HubProcessingException, HubMaintenanceException {
invoke(HttpMethod.GET, sceneCollectionActivate,
Query.of("sceneCollectionId", Integer.toString(sceneCollectionId)), null);
}
/** /**
* Invoke a call on the hub server to retrieve information or send a command * Invoke a call on the hub server to retrieve information or send a command
* *

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.hdpowerview.internal.api.responses;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* State of all Scenes in an HD PowerView hub
*
* @author Jacob Laursen - Initial contribution
*/
@NonNullByDefault
public class SceneCollections {
public @Nullable List<SceneCollection> sceneCollectionData;
public @Nullable List<Integer> sceneCollectionIds;
/*
* the following SuppressWarnings annotation is because the Eclipse compiler
* does NOT expect a NonNullByDefault annotation on the inner class, since it is
* implicitly inherited from the outer class, whereas the Maven compiler always
* requires an explicit NonNullByDefault annotation on all classes
*/
@SuppressWarnings("null")
@NonNullByDefault
public static class SceneCollection {
public int id;
public @Nullable String name;
public int order;
public int colorId;
public int iconId;
public String getName() {
return new String(Base64.getDecoder().decode(name), StandardCharsets.UTF_8);
}
}
}

View File

@ -12,6 +12,7 @@
*/ */
package org.openhab.binding.hdpowerview.internal.api.responses; package org.openhab.binding.hdpowerview.internal.api.responses;
import java.nio.charset.StandardCharsets;
import java.util.Base64; import java.util.Base64;
import java.util.List; import java.util.List;
@ -46,7 +47,7 @@ public class Scenes {
public int iconId; public int iconId;
public String getName() { public String getName() {
return new String(Base64.getDecoder().decode(name)); return new String(Base64.getDecoder().decode(name), StandardCharsets.UTF_8);
} }
} }
} }

View File

@ -26,9 +26,12 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants; import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants;
import org.openhab.binding.hdpowerview.internal.HDPowerViewTranslationProvider;
import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets; import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets;
import org.openhab.binding.hdpowerview.internal.HubMaintenanceException; import org.openhab.binding.hdpowerview.internal.HubMaintenanceException;
import org.openhab.binding.hdpowerview.internal.HubProcessingException; import org.openhab.binding.hdpowerview.internal.HubProcessingException;
import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections;
import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections.SceneCollection;
import org.openhab.binding.hdpowerview.internal.api.responses.Scenes; import org.openhab.binding.hdpowerview.internal.api.responses.Scenes;
import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene; import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene;
import org.openhab.binding.hdpowerview.internal.api.responses.Shades; import org.openhab.binding.hdpowerview.internal.api.responses.Shades;
@ -59,12 +62,14 @@ import com.google.gson.JsonParseException;
* *
* @author Andy Lintner - Initial contribution * @author Andy Lintner - Initial contribution
* @author Andrew Fiddian-Green - Added support for secondary rail positions * @author Andrew Fiddian-Green - Added support for secondary rail positions
* @author Jacob Laursen - Add support for scene groups
*/ */
@NonNullByDefault @NonNullByDefault
public class HDPowerViewHubHandler extends BaseBridgeHandler { public class HDPowerViewHubHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(HDPowerViewHubHandler.class); private final Logger logger = LoggerFactory.getLogger(HDPowerViewHubHandler.class);
private final HttpClient httpClient; private final HttpClient httpClient;
private final HDPowerViewTranslationProvider translationProvider;
private long refreshInterval; private long refreshInterval;
private long hardRefreshPositionInterval; private long hardRefreshPositionInterval;
@ -78,9 +83,14 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
private final ChannelTypeUID sceneChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID, private final ChannelTypeUID sceneChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID,
HDPowerViewBindingConstants.CHANNELTYPE_SCENE_ACTIVATE); HDPowerViewBindingConstants.CHANNELTYPE_SCENE_ACTIVATE);
public HDPowerViewHubHandler(Bridge bridge, HttpClient httpClient) { private final ChannelTypeUID sceneCollectionChannelTypeUID = new ChannelTypeUID(
HDPowerViewBindingConstants.BINDING_ID, HDPowerViewBindingConstants.CHANNELTYPE_SCENE_GROUP_ACTIVATE);
public HDPowerViewHubHandler(Bridge bridge, HttpClient httpClient,
HDPowerViewTranslationProvider translationProvider) {
super(bridge); super(bridge);
this.httpClient = httpClient; this.httpClient = httpClient;
this.translationProvider = translationProvider;
} }
@Override @Override
@ -90,21 +100,30 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
return; return;
} }
if (!OnOffType.ON.equals(command)) {
return;
}
Channel channel = getThing().getChannel(channelUID.getId()); Channel channel = getThing().getChannel(channelUID.getId());
if (channel != null && sceneChannelTypeUID.equals(channel.getChannelTypeUID())) { if (channel == null) {
if (OnOffType.ON.equals(command)) { return;
try { }
HDPowerViewWebTargets webTargets = this.webTargets;
if (webTargets == null) { try {
throw new ProcessingException("Web targets not initialized"); HDPowerViewWebTargets webTargets = this.webTargets;
} if (webTargets == null) {
webTargets.activateScene(Integer.parseInt(channelUID.getId())); throw new ProcessingException("Web targets not initialized");
} catch (HubMaintenanceException e) {
// exceptions are logged in HDPowerViewWebTargets
} catch (NumberFormatException | HubProcessingException e) {
logger.debug("Unexpected error {}", e.getMessage());
}
} }
int id = Integer.parseInt(channelUID.getId());
if (sceneChannelTypeUID.equals(channel.getChannelTypeUID())) {
webTargets.activateScene(id);
} else if (sceneCollectionChannelTypeUID.equals(channel.getChannelTypeUID())) {
webTargets.activateSceneCollection(id);
}
} catch (HubMaintenanceException e) {
// exceptions are logged in HDPowerViewWebTargets
} catch (NumberFormatException | HubProcessingException e) {
logger.debug("Unexpected error {}", e.getMessage());
} }
} }
@ -115,7 +134,8 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
String host = config.host; String host = config.host;
if (host == null || host.isEmpty()) { if (host == null || host.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Host address must be set"); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.conf-error-no-host-address");
return; return;
} }
@ -196,6 +216,7 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
logger.debug("Polling for state"); logger.debug("Polling for state");
pollShades(); pollShades();
pollScenes(); pollScenes();
pollSceneCollections();
} catch (JsonParseException e) { } catch (JsonParseException e) {
logger.warn("Bridge returned a bad JSON response: {}", e.getMessage()); logger.warn("Bridge returned a bad JSON response: {}", e.getMessage());
} catch (HubProcessingException e) { } catch (HubProcessingException e) {
@ -266,7 +287,9 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
} }
logger.debug("Received data for {} scenes", sceneData.size()); logger.debug("Received data for {} scenes", sceneData.size());
Map<String, Channel> idChannelMap = getIdChannelMap(); Map<String, Channel> idChannelMap = getIdSceneChannelMap();
List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
boolean isChannelListChanged = false;
for (Scene scene : sceneData) { for (Scene scene : sceneData) {
// remove existing scene channel from the map // remove existing scene channel from the map
String sceneId = Integer.toString(scene.id); String sceneId = Integer.toString(scene.id);
@ -276,9 +299,12 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
} else { } else {
// create a new scene channel // create a new scene channel
ChannelUID channelUID = new ChannelUID(getThing().getUID(), sceneId); 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) Channel channel = ChannelBuilder.create(channelUID, "Switch").withType(sceneChannelTypeUID)
.withLabel(scene.getName()).withDescription("Activates the scene " + scene.getName()).build(); .withLabel(scene.getName()).withDescription(description).build();
updateThing(editThing().withChannel(channel).build()); allChannels.add(channel);
isChannelListChanged = true;
logger.debug("Creating new channel for scene '{}'", sceneId); logger.debug("Creating new channel for scene '{}'", sceneId);
} }
} }
@ -286,8 +312,62 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
// remove any previously created channels that no longer exist // remove any previously created channels that no longer exist
if (!idChannelMap.isEmpty()) { if (!idChannelMap.isEmpty()) {
logger.debug("Removing {} orphan scene channels", idChannelMap.size()); logger.debug("Removing {} orphan scene channels", idChannelMap.size());
List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
allChannels.removeAll(idChannelMap.values()); allChannels.removeAll(idChannelMap.values());
isChannelListChanged = true;
}
if (isChannelListChanged) {
updateThing(editThing().withChannels(allChannels).build());
}
}
private void pollSceneCollections() throws JsonParseException, HubProcessingException, HubMaintenanceException {
HDPowerViewWebTargets webTargets = this.webTargets;
if (webTargets == null) {
throw new ProcessingException("Web targets not initialized");
}
SceneCollections sceneCollections = webTargets.getSceneCollections();
if (sceneCollections == null) {
throw new JsonParseException("Missing 'sceneCollections' element");
}
List<SceneCollection> sceneCollectionData = sceneCollections.sceneCollectionData;
if (sceneCollectionData == null) {
throw new JsonParseException("Missing 'sceneCollections.sceneCollectionData' element");
}
logger.debug("Received data for {} sceneCollections", sceneCollectionData.size());
Map<String, Channel> idChannelMap = getIdSceneCollectionChannelMap();
List<Channel> allChannels = new ArrayList<>(getThing().getChannels());
boolean isChannelListChanged = false;
for (SceneCollection sceneCollection : sceneCollectionData) {
// remove existing scene collection channel from the map
String sceneCollectionId = Integer.toString(sceneCollection.id);
if (idChannelMap.containsKey(sceneCollectionId)) {
idChannelMap.remove(sceneCollectionId);
logger.debug("Keeping channel for existing scene collection '{}'", sceneCollectionId);
} else {
// create a new scene collection channel
ChannelUID channelUID = new ChannelUID(getThing().getUID(), sceneCollectionId);
String description = translationProvider.getText("dynamic-channel.scene-group-activate.description",
sceneCollection.getName());
Channel channel = ChannelBuilder.create(channelUID, "Switch").withType(sceneCollectionChannelTypeUID)
.withLabel(sceneCollection.getName()).withDescription(description).build();
allChannels.add(channel);
isChannelListChanged = true;
logger.debug("Creating new channel for scene collection '{}'", sceneCollectionId);
}
}
// remove any previously created channels that no longer exist
if (!idChannelMap.isEmpty()) {
logger.debug("Removing {} orphan scene collection channels", idChannelMap.size());
allChannels.removeAll(idChannelMap.values());
isChannelListChanged = true;
}
if (isChannelListChanged) {
updateThing(editThing().withChannels(allChannels).build()); updateThing(editThing().withChannels(allChannels).build());
} }
} }
@ -313,7 +393,7 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
return ret; return ret;
} }
private Map<String, Channel> getIdChannelMap() { private Map<String, Channel> getIdSceneChannelMap() {
Map<String, Channel> ret = new HashMap<>(); Map<String, Channel> ret = new HashMap<>();
for (Channel channel : getThing().getChannels()) { for (Channel channel : getThing().getChannels()) {
if (sceneChannelTypeUID.equals(channel.getChannelTypeUID())) { if (sceneChannelTypeUID.equals(channel.getChannelTypeUID())) {
@ -323,6 +403,16 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler {
return ret; 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() { private void requestRefreshShadePositions() {
Map<Thing, String> thingIdMap = getThingIdMap(); Map<Thing, String> thingIdMap = getThingIdMap();
for (Entry<Thing, String> item : thingIdMap.entrySet()) { for (Entry<Thing, String> item : thingIdMap.entrySet()) {

View File

@ -0,0 +1,44 @@
# binding
binding.hdpowerview.name = Hunter Douglas PowerView Binding
binding.hdpowerview.description = The Hunter Douglas PowerView binding provides access to the Hunter Douglas line of PowerView shades.
# thing types
thing-type.hdpowerview.hub.label = PowerView Hub
thing-type.hdpowerview.hub.description = Hunter Douglas (Luxaflex) PowerView Hub
thing-type.hdpowerview.shade.label = PowerView Shade
thing-type.hdpowerview.shade.description = Hunter Douglas (Luxaflex) PowerView Shade
thing-type.hdpowerview.shade.channel.secondary.label = Secondary Position
thing-type.hdpowerview.shade.channel.secondary.description = The secondary vertical position (on top-down/bottom-up shades)
# thing types config
thing-type.config.hdpowerview.hub.hardRefresh.label = Hard Position Refresh Interval
thing-type.config.hdpowerview.hub.hardRefresh.description = The number of minutes between hard refreshes of positions from the PowerView Hub (or 0 to disable)
thing-type.config.hdpowerview.hub.hardRefreshBatteryLevel.label = Hard Battery Level Refresh Interval
thing-type.config.hdpowerview.hub.hardRefreshBatteryLevel.description = The number of hours between hard refreshes of battery levels from the PowerView Hub (or 0 to disable, default is weekly)
thing-type.config.hdpowerview.hub.host.label = Host
thing-type.config.hdpowerview.hub.host.description = The Host address of the PowerView Hub
thing-type.config.hdpowerview.hub.refresh.label = Refresh Interval
thing-type.config.hdpowerview.hub.refresh.description = The number of milliseconds between fetches of the PowerView Hub shade state
thing-type.config.hdpowerview.shade.id.label = ID
thing-type.config.hdpowerview.shade.id.description = The numeric ID of the PowerView Shade in the Hub
# channel types
channel-type.hdpowerview.battery-voltage.label = Battery Voltage
channel-type.hdpowerview.battery-voltage.description = Battery voltage reported by the shade
channel-type.hdpowerview.shade-position.label = Position
channel-type.hdpowerview.shade-position.description = The vertical position of the shade
channel-type.hdpowerview.shade-vane.label = Vane
channel-type.hdpowerview.shade-vane.description = The opening of the slats in the shade
# thing status descriptions
offline.conf-error-no-host-address = Host address must be set
# dynamic channels
dynamic-channel.scene-activate.description = Activates the scene ''{0}''
dynamic-channel.scene-group-activate.description = Activates the scene group ''{0}''

View File

@ -89,7 +89,11 @@
<channel-type id="scene-activate"> <channel-type id="scene-activate">
<item-type>Switch</item-type> <item-type>Switch</item-type>
<label>Activate</label> <label>Activate</label>
<description>Activates the scene</description> </channel-type>
<channel-type id="scene-group-activate">
<item-type>Switch</item-type>
<label>Activate</label>
</channel-type> </channel-type>
<channel-type id="battery-voltage" advanced="true"> <channel-type id="battery-voltage" advanced="true">

View File

@ -16,11 +16,12 @@ import static org.junit.jupiter.api.Assertions.*;
import static org.openhab.binding.hdpowerview.internal.api.ActuatorClass.*; import static org.openhab.binding.hdpowerview.internal.api.ActuatorClass.*;
import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*; import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List; import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@ -31,6 +32,8 @@ import org.openhab.binding.hdpowerview.internal.HubMaintenanceException;
import org.openhab.binding.hdpowerview.internal.HubProcessingException; import org.openhab.binding.hdpowerview.internal.HubProcessingException;
import org.openhab.binding.hdpowerview.internal.api.CoordinateSystem; import org.openhab.binding.hdpowerview.internal.api.CoordinateSystem;
import org.openhab.binding.hdpowerview.internal.api.ShadePosition; import org.openhab.binding.hdpowerview.internal.api.ShadePosition;
import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections;
import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections.SceneCollection;
import org.openhab.binding.hdpowerview.internal.api.responses.Scenes; import org.openhab.binding.hdpowerview.internal.api.responses.Scenes;
import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene; import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene;
import org.openhab.binding.hdpowerview.internal.api.responses.Shade; import org.openhab.binding.hdpowerview.internal.api.responses.Shade;
@ -47,6 +50,7 @@ import com.google.gson.JsonParseException;
* Unit tests for HD PowerView binding * Unit tests for HD PowerView binding
* *
* @author Andrew Fiddian-Green - Initial contribution * @author Andrew Fiddian-Green - Initial contribution
* @author Jacob Laursen - Add support for scene groups
*/ */
@NonNullByDefault @NonNullByDefault
public class HDPowerViewJUnitTests { public class HDPowerViewJUnitTests {
@ -58,14 +62,9 @@ public class HDPowerViewJUnitTests {
* load a test JSON string from a file * load a test JSON string from a file
*/ */
private String loadJson(String fileName) { private String loadJson(String fileName) {
try (FileReader file = new FileReader(String.format("src/test/resources/%s.json", fileName)); try {
BufferedReader reader = new BufferedReader(file)) { return Files.readAllLines(Paths.get(String.format("src/test/resources/%s.json", fileName))).stream()
StringBuilder builder = new StringBuilder(); .collect(Collectors.joining());
String line;
while ((line = reader.readLine()) != null) {
builder.append(line).append("\n");
}
return builder.toString();
} catch (IOException e) { } catch (IOException e) {
fail(e.getMessage()); fail(e.getMessage());
} }
@ -287,80 +286,107 @@ public class HDPowerViewJUnitTests {
} }
/** /**
* Run a series of OFFLINE tests on the JSON parsing machinery * Test generic JSON shades response
*/ */
@Test @Test
public void testOfflineJsonParsing() { public void shadeResponseIsParsedCorrectly() throws JsonParseException {
final Gson gson = new Gson(); final Gson gson = new Gson();
@Nullable @Nullable
Shades shades; Shades shades;
// test generic JSON shades response String json = loadJson("shades");
try { assertNotEquals("", json);
@Nullable shades = gson.fromJson(json, Shades.class);
String json = loadJson("shades"); assertNotNull(shades);
assertNotNull(json); }
assertNotEquals("", json);
shades = gson.fromJson(json, Shades.class);
assertNotNull(shades);
} catch (JsonParseException e) {
fail(e.getMessage());
}
// test generic JSON scenes response /**
try { * Test generic JSON scene response
@Nullable */
String json = loadJson("scenes"); @Test
assertNotNull(json); public void sceneResponseIsParsedCorrectly() throws JsonParseException {
assertNotEquals("", json); final Gson gson = new Gson();
@Nullable String json = loadJson("scenes");
Scenes scenes = gson.fromJson(json, Scenes.class); assertNotEquals("", json);
assertNotNull(scenes);
} catch (JsonParseException e) {
fail(e.getMessage());
}
// test the JSON parsing for a duette top down bottom up shade @Nullable
try { Scenes scenes = gson.fromJson(json, Scenes.class);
@Nullable assertNotNull(scenes);
ShadeData shadeData = null;
String json = loadJson("duette");
assertNotNull(json);
assertNotEquals("", json);
shades = gson.fromJson(json, Shades.class); @Nullable
assertNotNull(shades); List<Scene> sceneData = scenes.sceneData;
@Nullable assertNotNull(sceneData);
List<ShadeData> shadesData = shades.shadeData;
assertNotNull(shadesData);
assertEquals(1, shadesData.size()); assertEquals(4, sceneData.size());
shadeData = shadesData.get(0); @Nullable
assertNotNull(shadeData); Scene scene = sceneData.get(0);
assertEquals("Door Open", scene.getName());
assertEquals(18097, scene.id);
}
assertEquals("Gardin 1", shadeData.getName()); /**
assertEquals(63778, shadeData.id); * Test generic JSON scene collection response
*/
@Test
public void sceneCollectionResponseIsParsedCorrectly() throws JsonParseException {
final Gson gson = new Gson();
String json = loadJson("sceneCollections");
assertNotEquals("", json);
ShadePosition shadePos = shadeData.positions; @Nullable
assertNotNull(shadePos); SceneCollections sceneCollections = gson.fromJson(json, SceneCollections.class);
assertEquals(ZERO_IS_CLOSED, shadePos.getCoordinateSystem(PRIMARY_ACTUATOR)); assertNotNull(sceneCollections);
@Nullable
List<SceneCollection> sceneCollectionData = sceneCollections.sceneCollectionData;
assertNotNull(sceneCollectionData);
State pos = shadePos.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED); assertEquals(1, sceneCollectionData.size());
assertEquals(PercentType.class, pos.getClass()); @Nullable
assertEquals(59, ((PercentType) pos).intValue()); SceneCollection sceneCollection = sceneCollectionData.get(0);
assertEquals("Børn op", sceneCollection.getName());
assertEquals(27119, sceneCollection.id);
}
pos = shadePos.getState(SECONDARY_ACTUATOR, ZERO_IS_OPEN); /**
assertEquals(PercentType.class, pos.getClass()); * Test the JSON parsing for a duette top down bottom up shade
assertEquals(35, ((PercentType) pos).intValue()); */
@Test
public void duetteTopDownBottomUpShadeIsParsedCorrectly() throws JsonParseException {
final Gson gson = new Gson();
String json = loadJson("duette");
assertNotEquals("", json);
pos = shadePos.getState(PRIMARY_ACTUATOR, VANE_COORDS); @Nullable
assertEquals(UnDefType.class, pos.getClass()); Shades shades = gson.fromJson(json, Shades.class);
assertNotNull(shades);
@Nullable
List<ShadeData> shadesData = shades.shadeData;
assertNotNull(shadesData);
assertEquals(3, shadeData.batteryStatus); assertEquals(1, shadesData.size());
@Nullable
ShadeData shadeData = shadesData.get(0);
assertNotNull(shadeData);
assertEquals(4, shadeData.signalStrength); assertEquals("Gardin 1", shadeData.getName());
} catch (JsonParseException e) { assertEquals(63778, shadeData.id);
fail(e.getMessage());
} ShadePosition shadePos = shadeData.positions;
assertNotNull(shadePos);
assertEquals(ZERO_IS_CLOSED, shadePos.getCoordinateSystem(PRIMARY_ACTUATOR));
State pos = shadePos.getState(PRIMARY_ACTUATOR, ZERO_IS_CLOSED);
assertEquals(PercentType.class, pos.getClass());
assertEquals(59, ((PercentType) pos).intValue());
pos = shadePos.getState(SECONDARY_ACTUATOR, ZERO_IS_OPEN);
assertEquals(PercentType.class, pos.getClass());
assertEquals(35, ((PercentType) pos).intValue());
pos = shadePos.getState(PRIMARY_ACTUATOR, VANE_COORDS);
assertEquals(UnDefType.class, pos.getClass());
assertEquals(3, shadeData.batteryStatus);
assertEquals(4, shadeData.signalStrength);
} }
} }

View File

@ -0,0 +1,15 @@
{
"sceneCollectionIds": [
27119
],
"sceneCollectionData": [
{
"name": "QsO4cm4gb3A=",
"colorId": 12,
"iconId": 17,
"id": 27119,
"order": 0,
"hkAssist": false
}
]
}