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

View File

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

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

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

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;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
@ -46,7 +47,7 @@ public class Scenes {
public int iconId;
public String getName() {
return new String(Base64.getDecoder().decode(name));
return new String(Base64.getDecoder().decode(name), StandardCharsets.UTF_8);
}
}
}

View File

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

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">
<item-type>Switch</item-type>
<label>Activate</label>
<description>Activates the scene</description>
</channel-type>
<channel-type id="scene-group-activate">
<item-type>Switch</item-type>
<label>Activate</label>
</channel-type>
<channel-type id="battery-voltage" advanced="true">

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

View File

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