[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
12 changed files with 420 additions and 96 deletions

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
}
]
}