]> git.basschouten.com Git - openhab-addons.git/commitdiff
[boschshc] Add scenario channel (#15752)
authorPatrick <54861416+pat-git023@users.noreply.github.com>
Sat, 11 Nov 2023 13:22:52 +0000 (14:22 +0100)
committerGitHub <noreply@github.com>
Sat, 11 Nov 2023 13:22:52 +0000 (14:22 +0100)
Signed-off-by: Patrick Gell <patgit023@gmail.com>
15 files changed:
bundles/org.openhab.binding.boschshc/README.md
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/ScenarioHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResult.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Scenario.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/serialization/BoschServiceDataDeserializer.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/serialization/GsonUtils.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceState.java
bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties
bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml
bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/update/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPollingTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/ScenarioHandlerTest.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/serialization/BoschServiceDataDeserializerTest.java [new file with mode: 0644]

index 6be38759eab06082f048bc595e852d40fa11d5e7..3448c644c8f41960044ce616817f3fe7caec50fd 100644 (file)
@@ -4,6 +4,7 @@ Binding for the Bosch Smart Home.
 
 - [Bosch Smart Home Binding](#bosch-smart-home-binding)
   - [Supported Things](#supported-things)
+    - [Smart Home Controller](#smart-home-controller)
     - [In-Wall Switch](#in-wall-switch)
     - [Compact Smart Plug](#compact-smart-plug)
     - [Twinguard Smoke Detector](#twinguard-smoke-detector)
@@ -27,6 +28,16 @@ Binding for the Bosch Smart Home.
 
 ## Supported Things
 
+### Smart Home Controller
+The Smart Home Controller is the central hub that allows you to monitor and control your smart home devices from one place.
+
+**Bridge Type ID**: ``shc``
+
+| Channel Type ID    | Item Type | Writable | Description                                                             |
+|--------------------|-----------|:--------:|-------------------------------------------------------------------------|
+| scenario-triggered | String    | &#9744;  | Name of the triggered scenario (e.g. by the Universal Switch Flex)      | 
+| trigger-scenario   | String    | &#9745;  | Name of a scenario to be triggered on the Bosch Smart Home Controller.  |
+
 ### In-Wall Switch
 
 A simple light control.
index 8b70ce0a499871504cd5f09c6b15bd54ae0af037..d99006889148d43fc393f62f3493ef2cb2a308d4 100644 (file)
@@ -51,6 +51,8 @@ public class BoschSHCBindingConstants {
 
     // List of all Channel IDs
     // Auto-generated from thing-types.xml via script, don't modify
+    public static final String CHANNEL_SCENARIO_TRIGGERED = "scenario-triggered";
+    public static final String CHANNEL_TRIGGER_SCENARIO = "trigger-scenario";
     public static final String CHANNEL_POWER_SWITCH = "power-switch";
     public static final String CHANNEL_TEMPERATURE = "temperature";
     public static final String CHANNEL_TEMPERATURE_RATING = "temperature-rating";
index 54e14d749d33510b5631ca48d413705116bc1c2c..717ad89d375cbc2a9eefd5f447b5217fca9fe53d 100644 (file)
@@ -34,11 +34,13 @@ import org.eclipse.jetty.client.api.Request;
 import org.eclipse.jetty.client.api.Response;
 import org.eclipse.jetty.http.HttpStatus;
 import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
 import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario;
 import org.openhab.binding.boschshc.internal.discovery.ThingDiscoveryService;
 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
 import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
@@ -46,7 +48,9 @@ import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException;
 import org.openhab.binding.boschshc.internal.serialization.GsonUtils;
 import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
 import org.openhab.binding.boschshc.internal.services.dto.JsonRestExceptionResponse;
+import org.openhab.core.library.types.StringType;
 import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Channel;
 import org.openhab.core.thing.ChannelUID;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingStatus;
@@ -55,6 +59,7 @@ import org.openhab.core.thing.binding.BaseBridgeHandler;
 import org.openhab.core.thing.binding.ThingHandler;
 import org.openhab.core.thing.binding.ThingHandlerService;
 import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.FrameworkUtil;
 import org.slf4j.Logger;
@@ -99,8 +104,11 @@ public class BridgeHandler extends BaseBridgeHandler {
      */
     private @Nullable ThingDiscoveryService thingDiscoveryService;
 
+    private final ScenarioHandler scenarioHandler;
+
     public BridgeHandler(Bridge bridge) {
         super(bridge);
+        scenarioHandler = new ScenarioHandler();
 
         this.longPolling = new LongPolling(this.scheduler, this::handleLongPollResult, this::handleLongPollFailure);
     }
@@ -195,6 +203,11 @@ public class BridgeHandler extends BaseBridgeHandler {
     @Override
     public void handleCommand(ChannelUID channelUID, Command command) {
         // commands are handled by individual device handlers
+        BoschHttpClient localHttpClient = httpClient;
+        if (BoschSHCBindingConstants.CHANNEL_TRIGGER_SCENARIO.equals(channelUID.getId())
+                && !RefreshType.REFRESH.equals(command) && localHttpClient != null) {
+            scenarioHandler.triggerScenario(localHttpClient, command.toString());
+        }
     }
 
     /**
@@ -410,8 +423,15 @@ public class BridgeHandler extends BaseBridgeHandler {
      * @param result Results from Long Polling
      */
     private void handleLongPollResult(LongPollResult result) {
-        for (DeviceServiceData deviceServiceData : result.result) {
-            handleDeviceServiceData(deviceServiceData);
+        for (BoschSHCServiceState serviceState : result.result) {
+            if (serviceState instanceof DeviceServiceData deviceServiceData) {
+                handleDeviceServiceData(deviceServiceData);
+            } else if (serviceState instanceof Scenario scenario) {
+                final Channel channel = this.getThing().getChannel(BoschSHCBindingConstants.CHANNEL_SCENARIO_TRIGGERED);
+                if (channel != null && isLinked(channel.getUID())) {
+                    updateState(channel.getUID(), new StringType(scenario.name));
+                }
+            }
         }
     }
 
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/ScenarioHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/ScenarioHandler.java
new file mode 100644 (file)
index 0000000..54a080a
--- /dev/null
@@ -0,0 +1,110 @@
+/**
+ * Copyright (c) 2010-2023 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.boschshc.internal.devices.bridge;
+
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handler for executing a scenario.
+ *
+ * @author Patrick Gell - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class ScenarioHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    protected ScenarioHandler() {
+    }
+
+    public void triggerScenario(final BoschHttpClient httpClient, final String scenarioName) {
+
+        final Scenario[] scenarios;
+        try {
+            scenarios = getAvailableScenarios(httpClient);
+        } catch (BoschSHCException e) {
+            logger.debug("unable to read the available scenarios from Bosch Smart Home Conteroller", e);
+            return;
+        }
+        final Optional<Scenario> scenario = Arrays.stream(scenarios).filter(s -> s.name.equals(scenarioName))
+                .findFirst();
+        if (scenario.isPresent()) {
+            sendPOSTRequest(httpClient.getBoschSmartHomeUrl(String.format("scenarios/%s/triggers", scenario.get().id)),
+                    httpClient);
+        } else {
+            if (logger.isDebugEnabled()) {
+                logger.debug("Scenario '{}' was not found in the list of available scenarios {}", scenarioName,
+                        prettyLogScenarios(scenarios));
+            }
+        }
+    }
+
+    private Scenario[] getAvailableScenarios(final BoschHttpClient httpClient) throws BoschSHCException {
+        final Request request = httpClient.createRequest(httpClient.getBoschSmartHomeUrl("scenarios"), HttpMethod.GET);
+        try {
+            return httpClient.sendRequest(request, Scenario[].class, Scenario::isValid, null);
+        } catch (InterruptedException e) {
+            logger.debug("Scenario call was interrupted", e);
+            Thread.currentThread().interrupt();
+        } catch (TimeoutException e) {
+            logger.debug("Scenario call timed out", e);
+        } catch (ExecutionException e) {
+            logger.debug("Exception occurred during scenario call", e);
+        }
+
+        return new Scenario[] {};
+    }
+
+    private void sendPOSTRequest(final String url, final BoschHttpClient httpClient) {
+        try {
+            final Request request = httpClient.createRequest(url, HttpMethod.POST);
+            final ContentResponse response = request.send();
+            if (HttpStatus.ACCEPTED_202 != response.getStatus()) {
+                logger.debug("{} - {} failed with {}: {}", HttpMethod.POST, url, response.getStatus(),
+                        response.getContentAsString());
+            }
+        } catch (InterruptedException e) {
+            logger.debug("Scenario call was interrupted", e);
+            Thread.currentThread().interrupt();
+        } catch (TimeoutException e) {
+            logger.debug("Scenario call timed out", e);
+        } catch (ExecutionException e) {
+            logger.debug("Exception occurred during scenario call", e);
+        }
+    }
+
+    private String prettyLogScenarios(final Scenario[] scenarios) {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("[");
+        for (Scenario scenario : scenarios) {
+            builder.append("\n  ");
+            builder.append(scenario);
+        }
+        builder.append("\n]");
+        return builder.toString();
+    }
+}
index 1838c96b445f517532bc308b9e681e6b7bd6bc7d..70b8cbade032a0f6c0240e7aa0dbb74b3f052170 100644 (file)
@@ -14,6 +14,8 @@ package org.openhab.binding.boschshc.internal.devices.bridge.dto;
 
 import java.util.ArrayList;
 
+import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
+
 /**
  * Response of the Controller for a Long Poll API call.
  *
@@ -35,6 +37,6 @@ public class LongPollResult {
      * ],"jsonrpc":"2.0"}
      */
 
-    public ArrayList<DeviceServiceData> result;
+    public ArrayList<BoschSHCServiceState> result;
     public String jsonrpc;
 }
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Scenario.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Scenario.java
new file mode 100644 (file)
index 0000000..4440d8f
--- /dev/null
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2010-2023 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.boschshc.internal.devices.bridge.dto;
+
+import java.util.Arrays;
+
+import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
+
+/**
+ * A scenario as represented by the controller.
+ *
+ * Json example:
+ * {
+ * "@type": "scenarioTriggered",
+ * "name": "My scenario",
+ * "id": "509bd737-eed0-40b7-8caa-e8686a714399",
+ * "lastTimeTriggered": "1693758693032"
+ * }
+ *
+ * @author Patrick Gell - Initial contribution
+ */
+public class Scenario extends BoschSHCServiceState {
+
+    public String name;
+    public String id;
+    public String lastTimeTriggered;
+
+    public Scenario() {
+        super("scenarioTriggered");
+    }
+
+    public static Scenario createScenario(final String id, final String name, final String lastTimeTriggered) {
+        final Scenario scenario = new Scenario();
+
+        scenario.id = id;
+        scenario.name = name;
+        scenario.lastTimeTriggered = lastTimeTriggered;
+        return scenario;
+    }
+
+    public static Boolean isValid(Scenario[] scenarios) {
+        return Arrays.stream(scenarios).allMatch(scenario -> (scenario.id != null));
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("Scenario{");
+        sb.append("name='").append(name).append("'");
+        sb.append(", id='").append(id).append("'");
+        sb.append(", lastTimeTriggered='").append(lastTimeTriggered).append("'");
+        sb.append('}');
+        return sb.toString();
+    }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/serialization/BoschServiceDataDeserializer.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/serialization/BoschServiceDataDeserializer.java
new file mode 100644 (file)
index 0000000..c04d0bd
--- /dev/null
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2010-2023 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.boschshc.internal.serialization;
+
+import java.lang.reflect.Type;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario;
+import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+
+/**
+ * Utility class for JSON deserialization of device data and triggered scenarios using Google Gson.
+ *
+ * @author Patrick Gell - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class BoschServiceDataDeserializer implements JsonDeserializer<BoschSHCServiceState> {
+
+    @Nullable
+    @Override
+    public BoschSHCServiceState deserialize(JsonElement jsonElement, Type type,
+            JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+
+        JsonObject jsonObject = jsonElement.getAsJsonObject();
+        JsonElement dataType = jsonObject.get("@type");
+        switch (dataType.getAsString()) {
+            case "DeviceServiceData" -> {
+                var deviceServiceData = new DeviceServiceData();
+                deviceServiceData.deviceId = jsonObject.get("deviceId").getAsString();
+                deviceServiceData.state = jsonObject.get("state");
+                deviceServiceData.id = jsonObject.get("id").getAsString();
+                deviceServiceData.path = jsonObject.get("path").getAsString();
+                return deviceServiceData;
+            }
+            case "scenarioTriggered" -> {
+                var scenario = new Scenario();
+                scenario.id = jsonObject.get("id").getAsString();
+                scenario.name = jsonObject.get("name").getAsString();
+                scenario.lastTimeTriggered = jsonObject.get("lastTimeTriggered").getAsString();
+                return scenario;
+            }
+            default -> {
+                return new BoschSHCServiceState(dataType.getAsString());
+            }
+        }
+    }
+}
index efa652a50656c25c4f8ccdeb734cddde919b548e..0360370a0c4ba43b0ea2f131d7c62f98f836f5df 100644 (file)
@@ -13,6 +13,7 @@
 package org.openhab.binding.boschshc.internal.serialization;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
 
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
@@ -35,6 +36,7 @@ public final class GsonUtils {
      * This instance does not serialize or deserialize fields named <code>logger</code>.
      */
     public static final Gson DEFAULT_GSON_INSTANCE = new GsonBuilder()
+            .registerTypeAdapter(BoschSHCServiceState.class, new BoschServiceDataDeserializer())
             .addSerializationExclusionStrategy(new LoggerExclusionStrategy())
             .addDeserializationExclusionStrategy(new LoggerExclusionStrategy()).create();
 }
index 488356b30534f159ecd89d88a53e52bcce2317b9..7d906c67174490e03c067ae1054f021087bf2faa 100644 (file)
@@ -37,7 +37,7 @@ public class BoschSHCServiceState {
     @SerializedName("@type")
     public final String type;
 
-    protected BoschSHCServiceState(String type) {
+    public BoschSHCServiceState(String type) {
         this.type = type;
 
         if (stateType == null) {
index 2371ad36a11b5899c3d15ffad88cc169d5deacbb..f2eb06fccffc68e7c1732882d09c545d9a3abaed 100644 (file)
@@ -105,6 +105,8 @@ channel-type.boschshc.purity-rating.label = Purity Rating
 channel-type.boschshc.purity-rating.description = Rating of the air purity.
 channel-type.boschshc.purity.label = Purity
 channel-type.boschshc.purity.description = Purity of the air. A higher value indicates a higher pollution.
+channel-type.boschshc.scenario-triggered.label = Scenario Triggered
+channel-type.boschshc.scenario-triggered.description = Name of the triggered scenario
 channel-type.boschshc.setpoint-temperature.label = Setpoint Temperature
 channel-type.boschshc.setpoint-temperature.description = Desired temperature.
 channel-type.boschshc.silent-mode.label = Silent Mode
@@ -126,6 +128,8 @@ channel-type.boschshc.temperature-rating.state.option.MEDIUM = Medium Temperatur
 channel-type.boschshc.temperature-rating.state.option.BAD = Bad Temperature
 channel-type.boschshc.temperature.label = Temperature
 channel-type.boschshc.temperature.description = Current measured temperature.
+channel-type.boschshc.trigger-scenario.label = Trigger Scenario
+channel-type.boschshc.trigger-scenario.description = Name of the scenario to trigger
 channel-type.boschshc.valve-tappet-position.label = Valve Tappet Position
 channel-type.boschshc.valve-tappet-position.description = Current open ratio (0 to 100).
 
index 03354abe143222d61d1cfbe4e2529063cadbc315..1ff71e37d1bd872ae31a63b3770289682e491849 100644 (file)
@@ -9,6 +9,15 @@
                <label>Smart Home Controller</label>
                <description>The Bosch Smart Home Bridge representing the Bosch Smart Home Controller.</description>
 
+               <channels>
+                       <channel id="scenario-triggered" typeId="scenario-triggered"/>
+                       <channel id="trigger-scenario" typeId="trigger-scenario"/>
+               </channels>
+
+               <properties>
+                       <property name="thingTypeVersion">1</property>
+               </properties>
+
                <config-description-ref uri="thing-type:boschshc:bridge"/>
        </bridge-type>
 
                </state>
        </channel-type>
 
+       <channel-type id="scenario-triggered">
+               <item-type>String</item-type>
+               <label>Scenario Triggered</label>
+               <description>Name of the triggered scenario</description>
+               <state readOnly="true"/>
+       </channel-type>
+       <channel-type id="trigger-scenario">
+               <item-type>String</item-type>
+               <label>Trigger Scenario</label>
+               <description>Name of the scenario to trigger</description>
+       </channel-type>
+
 </thing:thing-descriptions>
diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/update/binding.xml b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/update/binding.xml
new file mode 100644 (file)
index 0000000..814f6b8
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
+<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
+       <thing-type uid="boschshc:shc">
+               <instruction-set targetVersion="1">
+                       <add-channel id="scenario-triggered">
+                               <type>boschshc:scenario-triggered</type>
+                       </add-channel>
+                       <add-channel id="trigger-scenario">
+                               <type>boschshc:trigger-scenario</type>
+                       </add-channel>
+               </instruction-set>
+       </thing-type>
+</update:update-descriptions>
index 33035a95b08afae9885c6d659946bfb9de573498..2b28b8d068b482fa734f3df994afcb6775e2c0fa 100644 (file)
@@ -48,6 +48,7 @@ import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult;
 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
 import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
@@ -237,7 +238,8 @@ class LongPollingTest {
         verify(longPollHandler).accept(longPollResultCaptor.capture());
         LongPollResult longPollResult = longPollResultCaptor.getValue();
         assertEquals(1, longPollResult.result.size());
-        DeviceServiceData longPollResultItem = longPollResult.result.get(0);
+        assertEquals(longPollResult.result.get(0).getClass(), DeviceServiceData.class);
+        DeviceServiceData longPollResultItem = (DeviceServiceData) longPollResult.result.get(0);
         assertEquals("hdm:HomeMaticIP:3014F711A0001916D859A8A9", longPollResultItem.deviceId);
         assertEquals("/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch", longPollResultItem.path);
         assertEquals("PowerSwitch", longPollResultItem.id);
@@ -246,6 +248,48 @@ class LongPollingTest {
         assertEquals("ON", stateObject.get("switchState").getAsString());
     }
 
+    @Test
+    void startLongPolling_receiveScenario()
+            throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+        // when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod();
+        when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod();
+
+        Request subscribeRequest = mock(Request.class);
+        when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
+                argThat((JsonRpcRequest r) -> "RE/subscribe".equals(r.method)))).thenReturn(subscribeRequest);
+        SubscribeResult subscribeResult = new SubscribeResult();
+        when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenReturn(subscribeResult);
+
+        Request longPollRequest = mock(Request.class);
+        when(httpClient.createRequest(anyString(), same(HttpMethod.POST),
+                argThat((JsonRpcRequest r) -> "RE/longPoll".equals(r.method)))).thenReturn(longPollRequest);
+
+        fixture.start(httpClient);
+
+        ArgumentCaptor<CompleteListener> completeListener = ArgumentCaptor.forClass(CompleteListener.class);
+        verify(longPollRequest).send(completeListener.capture());
+
+        BufferingResponseListener bufferingResponseListener = (BufferingResponseListener) completeListener.getValue();
+
+        String longPollResultJSON = "{\"result\":[{\"@type\": \"scenarioTriggered\",\"name\": \"My scenario\",\"id\": \"509bd737-eed0-40b7-8caa-e8686a714399\",\"lastTimeTriggered\": \"1693758693032\"}],\"jsonrpc\":\"2.0\"}\n";
+        Response response = mock(Response.class);
+        bufferingResponseListener.onContent(response,
+                ByteBuffer.wrap(longPollResultJSON.getBytes(StandardCharsets.UTF_8)));
+
+        Result result = mock(Result.class);
+        bufferingResponseListener.onComplete(result);
+
+        ArgumentCaptor<LongPollResult> longPollResultCaptor = ArgumentCaptor.forClass(LongPollResult.class);
+        verify(longPollHandler).accept(longPollResultCaptor.capture());
+        LongPollResult longPollResult = longPollResultCaptor.getValue();
+        assertEquals(1, longPollResult.result.size());
+        assertEquals(longPollResult.result.get(0).getClass(), Scenario.class);
+        Scenario longPollResultItem = (Scenario) longPollResult.result.get(0);
+        assertEquals("509bd737-eed0-40b7-8caa-e8686a714399", longPollResultItem.id);
+        assertEquals("My scenario", longPollResultItem.name);
+        assertEquals("1693758693032", longPollResultItem.lastTimeTriggered);
+    }
+
     @Test
     void startSubscriptionFailure()
             throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/ScenarioHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/ScenarioHandlerTest.java
new file mode 100644 (file)
index 0000000..3035a6f
--- /dev/null
@@ -0,0 +1,172 @@
+/**
+ * Copyright (c) 2010-2023 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.boschshc.internal.devices.bridge;
+
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+
+/**
+ * Unit tests for {@link ScenarioHandler}.
+ *
+ * @author Patrick Gell - Initial contribution
+ *
+ */
+@NonNullByDefault
+@ExtendWith(MockitoExtension.class)
+class ScenarioHandlerTest {
+
+    private final Scenario[] existingScenarios = List.of(
+            Scenario.createScenario(UUID.randomUUID().toString(), "Scenario 1",
+                    String.valueOf(System.currentTimeMillis())),
+            Scenario.createScenario(UUID.randomUUID().toString(), "Scenario 2",
+                    String.valueOf(System.currentTimeMillis()))
+
+    ).toArray(Scenario[]::new);
+
+    protected static Exception[] exceptionData() {
+        return List.of(new BoschSHCException(), new InterruptedException(), new TimeoutException(),
+                new ExecutionException(new BoschSHCException())).toArray(Exception[]::new);
+    }
+
+    protected static Exception[] httpExceptionData() {
+        return List
+                .of(new InterruptedException(), new TimeoutException(), new ExecutionException(new BoschSHCException()))
+                .toArray(Exception[]::new);
+    }
+
+    @Test
+    void triggerScenario_ShouldSendPOST_ToBoschAPI() throws Exception {
+        // GIVEN
+        final var httpClient = mock(BoschHttpClient.class);
+        final var request = mock(Request.class);
+        final var contentResponse = mock(ContentResponse.class);
+        when(httpClient.getBoschSmartHomeUrl(anyString())).thenReturn("http://localhost/smartHome/scenarios")
+                .thenReturn("http://localhost/smartHome/scenarios/1234/triggers");
+        when(httpClient.createRequest(anyString(), any(HttpMethod.class))).thenReturn(request).thenReturn(request);
+        when(httpClient.sendRequest(any(Request.class), any(), any(), any())).thenReturn(existingScenarios);
+        when(request.send()).thenReturn(contentResponse);
+        when(contentResponse.getStatus()).thenReturn(HttpStatus.OK_200);
+
+        final var handler = new ScenarioHandler();
+
+        // WHEN
+        handler.triggerScenario(httpClient, "Scenario 1");
+
+        // THEN
+        verify(httpClient).getBoschSmartHomeUrl("scenarios");
+        verify(request).send();
+    }
+
+    @Test
+    void triggerScenario_ShouldNoSendPOST_ToScenarioNameDoesNotExist() throws Exception {
+        // GIVEN
+        final var httpClient = mock(BoschHttpClient.class);
+        final var request = mock(Request.class);
+        when(httpClient.getBoschSmartHomeUrl(anyString())).thenReturn("http://localhost/smartHome/scenarios")
+                .thenReturn("http://localhost/smartHome/scenarios/1234/triggers");
+        when(httpClient.createRequest(anyString(), any(HttpMethod.class))).thenReturn(request).thenReturn(request);
+        when(httpClient.sendRequest(any(Request.class), any(), any(), any())).thenReturn(existingScenarios);
+
+        final var handler = new ScenarioHandler();
+
+        // WHEN
+        handler.triggerScenario(httpClient, "not existing Scenario");
+
+        // THEN
+        verify(httpClient).getBoschSmartHomeUrl("scenarios");
+        verify(request, times(0)).send();
+    }
+
+    @ParameterizedTest
+    @MethodSource("exceptionData")
+    void triggerScenario_ShouldNotPanic_IfBoschAPIThrowsException(final Exception exception) throws Exception {
+        // GIVEN
+        final var httpClient = mock(BoschHttpClient.class);
+        final var request = mock(Request.class);
+        when(httpClient.getBoschSmartHomeUrl(anyString())).thenReturn("http://localhost/smartHome/scenarios")
+                .thenReturn("http://localhost/smartHome/scenarios/1234/triggers");
+        when(httpClient.createRequest(anyString(), any(HttpMethod.class))).thenReturn(request);
+        when(httpClient.sendRequest(any(Request.class), any(), any(), any())).thenThrow(exception);
+
+        final var handler = new ScenarioHandler();
+
+        // WHEN
+        handler.triggerScenario(httpClient, "Scenario 1");
+
+        // THEN
+        verify(httpClient).getBoschSmartHomeUrl("scenarios");
+        verify(request, times(0)).send();
+    }
+
+    @Test
+    void triggerScenario_ShouldNotPanic_IfPOSTIsNotSuccessful() throws Exception {
+        // GIVEN
+        final var httpClient = mock(BoschHttpClient.class);
+        final var request = mock(Request.class);
+        final var contentResponse = mock(ContentResponse.class);
+        when(httpClient.getBoschSmartHomeUrl(anyString())).thenReturn("http://localhost/smartHome/scenarios")
+                .thenReturn("http://localhost/smartHome/scenarios/1234/triggers");
+        when(httpClient.createRequest(anyString(), any(HttpMethod.class))).thenReturn(request).thenReturn(request);
+        when(httpClient.sendRequest(any(Request.class), any(), any(), any())).thenReturn(existingScenarios);
+        when(request.send()).thenReturn(contentResponse);
+        when(contentResponse.getStatus()).thenReturn(HttpStatus.METHOD_NOT_ALLOWED_405);
+
+        final var handler = new ScenarioHandler();
+
+        // WHEN
+        handler.triggerScenario(httpClient, "Scenario 1");
+
+        // THEN
+        verify(httpClient).getBoschSmartHomeUrl("scenarios");
+        verify(request).send();
+    }
+
+    @ParameterizedTest
+    @MethodSource("httpExceptionData")
+    void triggerScenario_ShouldNotPanic_IfPOSTThrowsException(final Exception exception) throws Exception {
+        // GIVEN
+        final var httpClient = mock(BoschHttpClient.class);
+        final var request = mock(Request.class);
+        when(httpClient.getBoschSmartHomeUrl(anyString())).thenReturn("http://localhost/smartHome/scenarios")
+                .thenReturn("http://localhost/smartHome/scenarios/1234/triggers");
+        when(httpClient.createRequest(anyString(), any(HttpMethod.class))).thenReturn(request).thenReturn(request);
+        when(httpClient.sendRequest(any(Request.class), any(), any(), any())).thenReturn(existingScenarios);
+        when(request.send()).thenThrow(exception);
+
+        final var handler = new ScenarioHandler();
+
+        // WHEN
+        handler.triggerScenario(httpClient, "Scenario 1");
+
+        // THEN
+        verify(httpClient).getBoschSmartHomeUrl("scenarios");
+        verify(request).send();
+    }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/serialization/BoschServiceDataDeserializerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/serialization/BoschServiceDataDeserializerTest.java
new file mode 100644 (file)
index 0000000..c06753d
--- /dev/null
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) 2010-2023 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.boschshc.internal.serialization;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.HashSet;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario;
+
+/**
+ * Unit tests for {@link BoschServiceDataDeserializer}.
+ *
+ * @author Patrick Gell - Initial contribution
+ *
+ */
+@NonNullByDefault
+class BoschServiceDataDeserializerTest {
+
+    @Test
+    void deserializationOfLongPollingResult() {
+        var resultJson = """
+                {
+                    "result": [
+                        {
+                            "@type": "scenarioTriggered",
+                            "name": "MyTriggeredScenario",
+                            "id": "509bd737-eed0-40b7-8caa-e8686a714399",
+                            "lastTimeTriggered": "1689417526720"
+                        },
+                        {
+                            "path":"/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch",
+                            "@type":"DeviceServiceData",
+                            "id":"PowerSwitch",
+                            "state":{
+                                "@type":"powerSwitchState",
+                                "switchState":"ON"
+                            },
+                            "deviceId":"hdm:HomeMaticIP:3014F711A0001916D859A8A9"
+                        }
+                    ],
+                    "jsonrpc": "2.0"
+                }
+                """;
+
+        var longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(resultJson, LongPollResult.class);
+        assertNotNull(longPollResult);
+        assertEquals(2, longPollResult.result.size());
+
+        var resultClasses = new HashSet<>(longPollResult.result.stream().map(e -> e.getClass().getName()).toList());
+        assertEquals(2, resultClasses.size());
+        assertTrue(resultClasses.contains(DeviceServiceData.class.getName()));
+        assertTrue(resultClasses.contains(Scenario.class.getName()));
+    }
+}