[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:
parent
2973f6d890
commit
8c83c27c57
@ -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
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
*
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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}''
|
||||
@ -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">
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
{
|
||||
"sceneCollectionIds": [
|
||||
27119
|
||||
],
|
||||
"sceneCollectionData": [
|
||||
{
|
||||
"name": "QsO4cm4gb3A=",
|
||||
"colorId": 12,
|
||||
"iconId": 17,
|
||||
"id": 27119,
|
||||
"order": 0,
|
||||
"hkAssist": false
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user