[boschshc] Add scenario channel (#15752)

Signed-off-by: Patrick Gell <patgit023@gmail.com>
This commit is contained in:
Patrick 2023-11-11 14:22:52 +01:00 committed by GitHub
parent 1eacf67f34
commit 1b466fb319
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 609 additions and 5 deletions

View File

@ -4,6 +4,7 @@ Binding for the Bosch Smart Home.
- [Bosch Smart Home Binding](#bosch-smart-home-binding) - [Bosch Smart Home Binding](#bosch-smart-home-binding)
- [Supported Things](#supported-things) - [Supported Things](#supported-things)
- [Smart Home Controller](#smart-home-controller)
- [In-Wall Switch](#in-wall-switch) - [In-Wall Switch](#in-wall-switch)
- [Compact Smart Plug](#compact-smart-plug) - [Compact Smart Plug](#compact-smart-plug)
- [Twinguard Smoke Detector](#twinguard-smoke-detector) - [Twinguard Smoke Detector](#twinguard-smoke-detector)
@ -27,6 +28,16 @@ Binding for the Bosch Smart Home.
## Supported Things ## 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 ### In-Wall Switch
A simple light control. A simple light control.

View File

@ -51,6 +51,8 @@ public class BoschSHCBindingConstants {
// List of all Channel IDs // List of all Channel IDs
// Auto-generated from thing-types.xml via script, don't modify // 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_POWER_SWITCH = "power-switch";
public static final String CHANNEL_TEMPERATURE = "temperature"; public static final String CHANNEL_TEMPERATURE = "temperature";
public static final String CHANNEL_TEMPERATURE_RATING = "temperature-rating"; public static final String CHANNEL_TEMPERATURE_RATING = "temperature-rating";

View File

@ -34,11 +34,13 @@ import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.ssl.SslContextFactory; 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.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device; 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.DeviceServiceData;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult; 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.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.discovery.ThingDiscoveryService;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException; 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.serialization.GsonUtils;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState; import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
import org.openhab.binding.boschshc.internal.services.dto.JsonRestExceptionResponse; 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.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus; 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.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.osgi.framework.Bundle; import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil; import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -99,8 +104,11 @@ public class BridgeHandler extends BaseBridgeHandler {
*/ */
private @Nullable ThingDiscoveryService thingDiscoveryService; private @Nullable ThingDiscoveryService thingDiscoveryService;
private final ScenarioHandler scenarioHandler;
public BridgeHandler(Bridge bridge) { public BridgeHandler(Bridge bridge) {
super(bridge); super(bridge);
scenarioHandler = new ScenarioHandler();
this.longPolling = new LongPolling(this.scheduler, this::handleLongPollResult, this::handleLongPollFailure); this.longPolling = new LongPolling(this.scheduler, this::handleLongPollResult, this::handleLongPollFailure);
} }
@ -195,6 +203,11 @@ public class BridgeHandler extends BaseBridgeHandler {
@Override @Override
public void handleCommand(ChannelUID channelUID, Command command) { public void handleCommand(ChannelUID channelUID, Command command) {
// commands are handled by individual device handlers // 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 * @param result Results from Long Polling
*/ */
private void handleLongPollResult(LongPollResult result) { private void handleLongPollResult(LongPollResult result) {
for (DeviceServiceData deviceServiceData : result.result) { for (BoschSHCServiceState serviceState : result.result) {
handleDeviceServiceData(deviceServiceData); 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));
}
}
} }
} }

View File

@ -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();
}
}

View File

@ -14,6 +14,8 @@ package org.openhab.binding.boschshc.internal.devices.bridge.dto;
import java.util.ArrayList; import java.util.ArrayList;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
/** /**
* Response of the Controller for a Long Poll API call. * Response of the Controller for a Long Poll API call.
* *
@ -35,6 +37,6 @@ public class LongPollResult {
* ],"jsonrpc":"2.0"} * ],"jsonrpc":"2.0"}
*/ */
public ArrayList<DeviceServiceData> result; public ArrayList<BoschSHCServiceState> result;
public String jsonrpc; public String jsonrpc;
} }

View File

@ -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();
}
}

View File

@ -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());
}
}
}
}

View File

@ -13,6 +13,7 @@
package org.openhab.binding.boschshc.internal.serialization; package org.openhab.binding.boschshc.internal.serialization;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; 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>. * This instance does not serialize or deserialize fields named <code>logger</code>.
*/ */
public static final Gson DEFAULT_GSON_INSTANCE = new GsonBuilder() public static final Gson DEFAULT_GSON_INSTANCE = new GsonBuilder()
.registerTypeAdapter(BoschSHCServiceState.class, new BoschServiceDataDeserializer())
.addSerializationExclusionStrategy(new LoggerExclusionStrategy()) .addSerializationExclusionStrategy(new LoggerExclusionStrategy())
.addDeserializationExclusionStrategy(new LoggerExclusionStrategy()).create(); .addDeserializationExclusionStrategy(new LoggerExclusionStrategy()).create();
} }

View File

@ -37,7 +37,7 @@ public class BoschSHCServiceState {
@SerializedName("@type") @SerializedName("@type")
public final String type; public final String type;
protected BoschSHCServiceState(String type) { public BoschSHCServiceState(String type) {
this.type = type; this.type = type;
if (stateType == null) { if (stateType == null) {

View 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-rating.description = Rating of the air purity.
channel-type.boschshc.purity.label = 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.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.label = Setpoint Temperature
channel-type.boschshc.setpoint-temperature.description = Desired temperature. channel-type.boschshc.setpoint-temperature.description = Desired temperature.
channel-type.boschshc.silent-mode.label = Silent Mode 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-rating.state.option.BAD = Bad Temperature
channel-type.boschshc.temperature.label = Temperature channel-type.boschshc.temperature.label = Temperature
channel-type.boschshc.temperature.description = Current measured 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.label = Valve Tappet Position
channel-type.boschshc.valve-tappet-position.description = Current open ratio (0 to 100). channel-type.boschshc.valve-tappet-position.description = Current open ratio (0 to 100).

View File

@ -9,6 +9,15 @@
<label>Smart Home Controller</label> <label>Smart Home Controller</label>
<description>The Bosch Smart Home Bridge representing the Bosch Smart Home Controller.</description> <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"/> <config-description-ref uri="thing-type:boschshc:bridge"/>
</bridge-type> </bridge-type>
@ -520,4 +529,16 @@
</state> </state>
</channel-type> </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> </thing:thing-descriptions>

View File

@ -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>

View File

@ -48,6 +48,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; 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.DeviceServiceData;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult; 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.devices.bridge.dto.SubscribeResult;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException; import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
@ -237,7 +238,8 @@ class LongPollingTest {
verify(longPollHandler).accept(longPollResultCaptor.capture()); verify(longPollHandler).accept(longPollResultCaptor.capture());
LongPollResult longPollResult = longPollResultCaptor.getValue(); LongPollResult longPollResult = longPollResultCaptor.getValue();
assertEquals(1, longPollResult.result.size()); 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("hdm:HomeMaticIP:3014F711A0001916D859A8A9", longPollResultItem.deviceId);
assertEquals("/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch", longPollResultItem.path); assertEquals("/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch", longPollResultItem.path);
assertEquals("PowerSwitch", longPollResultItem.id); assertEquals("PowerSwitch", longPollResultItem.id);
@ -246,6 +248,48 @@ class LongPollingTest {
assertEquals("ON", stateObject.get("switchState").getAsString()); 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 @Test
void startSubscriptionFailure() void startSubscriptionFailure()
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {

View File

@ -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();
}
}

View File

@ -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()));
}
}