diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewBindingConstants.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewBindingConstants.java index da235dd22c8da..c67861c29069b 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewBindingConstants.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewBindingConstants.java @@ -26,6 +26,7 @@ * * @author Andy Lintner - Initial contribution * @author Andrew Fiddian-Green - Added support for secondary rail positions + * @author Jacob Laursen - Add support for scene collections */ @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_COLLECTION_ACTIVATE = "scene-collection-activate"; public static final List NETBIOS_NAMES = Arrays.asList("PDBU-Hub3.0", "PowerView-Hub"); diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java index eaa17cfbfe3ac..846d63399b3da 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java @@ -28,6 +28,7 @@ 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 @@ * * @author Andy Lintner - Initial contribution * @author Andrew Fiddian-Green - Added support for secondary rail positions + * @author Jacob Laursen - Add support for scene collections */ @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 HDPowerViewWebTargets(HttpClient httpClient, String ipAddress) { shades = base + "shades/"; sceneActivate = base + "scenes"; scenes = base + "scenes/"; + sceneCollectionActivate = base + "sceneCollections"; + sceneCollections = base + "sceneCollections/"; this.httpClient = httpClient; } @@ -156,6 +162,33 @@ public void activateScene(int sceneId) throws HubProcessingException, HubMainten 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 * diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/SceneCollections.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/SceneCollections.java new file mode 100644 index 0000000000000..90546ca1aa4ba --- /dev/null +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/SceneCollections.java @@ -0,0 +1,51 @@ +/** + * 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.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 sceneCollectionData; + public @Nullable List 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)); + } + } +} diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java index d617bb06cfbeb..1c9d757111e8a 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java @@ -29,6 +29,8 @@ 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,6 +61,7 @@ * * @author Andy Lintner - Initial contribution * @author Andrew Fiddian-Green - Added support for secondary rail positions + * @author Jacob Laursen - Add support for scene collections */ @NonNullByDefault public class HDPowerViewHubHandler extends BaseBridgeHandler { @@ -78,6 +81,9 @@ public class HDPowerViewHubHandler extends BaseBridgeHandler { private final ChannelTypeUID sceneChannelTypeUID = new ChannelTypeUID(HDPowerViewBindingConstants.BINDING_ID, HDPowerViewBindingConstants.CHANNELTYPE_SCENE_ACTIVATE); + private final ChannelTypeUID sceneCollectionChannelTypeUID = new ChannelTypeUID( + HDPowerViewBindingConstants.BINDING_ID, HDPowerViewBindingConstants.CHANNELTYPE_SCENE_COLLECTION_ACTIVATE); + public HDPowerViewHubHandler(Bridge bridge, HttpClient httpClient) { super(bridge); this.httpClient = httpClient; @@ -90,21 +96,30 @@ public void handleCommand(ChannelUID channelUID, Command command) { 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()); } } @@ -196,6 +211,7 @@ private synchronized void poll() { 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 +282,7 @@ private void pollScenes() throws JsonParseException, HubProcessingException, Hub } logger.debug("Received data for {} scenes", sceneData.size()); - Map idChannelMap = getIdChannelMap(); + Map idChannelMap = getIdSceneChannelMap(); for (Scene scene : sceneData) { // remove existing scene channel from the map String sceneId = Integer.toString(scene.id); @@ -292,6 +308,50 @@ private void pollScenes() throws JsonParseException, HubProcessingException, Hub } } + 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 sceneCollectionData = sceneCollections.sceneCollectionData; + if (sceneCollectionData == null) { + throw new JsonParseException("Missing 'sceneCollections.sceneCollectionData' element"); + } + logger.debug("Received data for {} sceneCollections", sceneCollectionData.size()); + + Map idChannelMap = getIdSceneCollectionChannelMap(); + for (SceneCollection sceneCollection : sceneCollectionData) { + // remove existing scene 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 channel + ChannelUID channelUID = new ChannelUID(getThing().getUID(), sceneCollectionId); + Channel channel = ChannelBuilder.create(channelUID, "Switch").withType(sceneCollectionChannelTypeUID) + .withLabel(sceneCollection.getName()) + .withDescription("Activates the scene collection " + sceneCollection.getName()).build(); + updateThing(editThing().withChannel(channel).build()); + 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()); + List allChannels = new ArrayList<>(getThing().getChannels()); + allChannels.removeAll(idChannelMap.values()); + updateThing(editThing().withChannels(allChannels).build()); + } + } + private Map getThingIdMap() { Map ret = new HashMap<>(); for (Thing thing : getThing().getThings()) { @@ -313,7 +373,7 @@ private Map getIdShadeDataMap(List shadeData) { return ret; } - private Map getIdChannelMap() { + private Map getIdSceneChannelMap() { Map ret = new HashMap<>(); for (Channel channel : getThing().getChannels()) { if (sceneChannelTypeUID.equals(channel.getChannelTypeUID())) { @@ -323,6 +383,16 @@ private Map getIdChannelMap() { return ret; } + private Map getIdSceneCollectionChannelMap() { + Map 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 thingIdMap = getThingIdMap(); for (Entry item : thingIdMap.entrySet()) { diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/thing/thing-types.xml index c852a49b75299..c0ac6313d0571 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.hdpowerview/src/main/resources/OH-INF/thing/thing-types.xml @@ -92,6 +92,12 @@ Activates the scene + + Switch + + Activates the scene collection + + Number:ElectricPotential