Skip to content

Commit

Permalink
chore: execute scenario
Browse files Browse the repository at this point in the history
Signed-off-by: Patrick Gell <[email protected]>

Signed-off-by: Patrick Gell <[email protected]>
  • Loading branch information
patrickgell authored and pat-git023 committed Sep 21, 2023
1 parent 94cf11f commit c643d58
Show file tree
Hide file tree
Showing 6 changed files with 350 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ 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";
public static final String CHANNEL_EXECUTE_SCENARIO = "execute-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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
Expand Down Expand Up @@ -102,8 +103,11 @@ public class BridgeHandler extends BaseBridgeHandler {
*/
private @Nullable ThingDiscoveryService thingDiscoveryService;

private final @NonNullByDefault ScenarioHandler scenarioHandler;

public BridgeHandler(Bridge bridge) {
super(bridge);
scenarioHandler = new ScenarioHandler(new HashMap<>());

this.longPolling = new LongPolling(this.scheduler, this::handleLongPollResult, this::handleLongPollFailure);
}
Expand Down Expand Up @@ -198,6 +202,10 @@ public void dispose() {
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// commands are handled by individual device handlers
if (channelUID.getId().equals(BoschSHCBindingConstants.CHANNEL_EXECUTE_SCENARIO)
&& !command.toString().equals("REFRESH") && this.httpClient != null) {
this.scenarioHandler.executeScenario(this.httpClient, command.toString());
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* 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.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
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.serialization.GsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.JsonSyntaxException;

/**
* Handler for executing a scenario.
*
* @author Patrick Gell - Initial contribution
*
*/
@NonNullByDefault
public class ScenarioHandler {

private final Logger logger = LoggerFactory.getLogger(getClass());

private final Map<String, Scenario> availableScenarios;

protected ScenarioHandler(Map<String, Scenario> availableScenarios) {
this.availableScenarios = Objects.requireNonNullElseGet(availableScenarios, HashMap::new);
}

public void executeScenario(final @Nullable BoschHttpClient httpClient, final String scenarioName) {
assert httpClient != null;
if (!availableScenarios.containsKey(scenarioName)) {
updateScenarios(httpClient);
}
final Scenario scenario = this.availableScenarios.get(scenarioName);
if (scenario != null) {
sendRequest(HttpMethod.POST,
httpClient.getBoschSmartHomeUrl(String.format("scenarios/%s/triggers", scenario.id)), httpClient);
} else {
logger.debug("scenario '{}' not found on the Bosch Controller", scenarioName);
}
}

private void updateScenarios(final @Nullable BoschHttpClient httpClient) {
if (httpClient != null) {
final String result = sendRequest(HttpMethod.GET, httpClient.getBoschSmartHomeUrl("scenarios"), httpClient);
try {
Scenario[] scenarios = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(result, Scenario[].class);
if (scenarios != null) {
for (Scenario scenario : scenarios) {
availableScenarios.put(scenario.name, scenario);
}
}
} catch (JsonSyntaxException e) {
logger.debug("response from SHC could not be parsed: {}", result, e);
}
}
}

private String sendRequest(final HttpMethod method, final String url, final BoschHttpClient httpClient) {
try {
final Request request = httpClient.createRequest(url, method);
final ContentResponse response = request.send();
switch (HttpStatus.getCode(response.getStatus())) {
case OK -> {
return response.getContentAsString();
}
case NOT_FOUND, METHOD_NOT_ALLOWED -> logger.debug("{} - {} failed with {}: {}", method, url,
response.getStatus(), response.getContentAsString());
}
} catch (InterruptedException e) {
logger.debug("scenario call was interrupted", e);
} catch (TimeoutException e) {
logger.debug("scenarion call timed out", e);
} catch (ExecutionException e) {
logger.debug("exception occurred during scenario call", e);
}
return "";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

<channels>
<channel id="triggered-scenario" typeId="triggered-scenario"/>
<channel id="execute-scenario" typeId="execute-scenario"/>
</channels>

<config-description-ref uri="thing-type:boschshc:bridge"/>
Expand Down Expand Up @@ -530,5 +531,10 @@
<description>Name of the triggered scenario</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="execute-scenario">
<item-type>String</item-type>
<label>Execute Scenario</label>
<description>Name of the scenario to execute</description>
</channel-type>

</thing:thing-descriptions>
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/**
* 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.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.*;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.stream.Stream;

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.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario;

/**
* Unit tests for {@link ScenarioHandler}.
*
* @author Patrick Gell - Initial contribution
*
*/
@NonNullByDefault
@ExtendWith(MockitoExtension.class)
public class ScenarioHandlerTest {

@Test
public void executeScenario_ShouldLoadAllScenarios_IfAvailableScenariosAreEmpty() throws Exception {
// GIVEN
final var httpClient = mock(BoschHttpClient.class);
final var request = mock(Request.class);
final var response = mock(ContentResponse.class);
when(httpClient.getBoschSmartHomeUrl("scenarios")).thenReturn("http://localhost/smartHome/scenarios");
when(httpClient.createRequest(anyString(), any(HttpMethod.class))).thenReturn(request);
when(request.send()).thenReturn(response);
when(response.getStatus()).thenReturn(200);
when(response.getContentAsString()).thenReturn(getJsonStringFromFile());

final Map<String, Scenario> availableScenarios = new HashMap<>();
final var handler = new ScenarioHandler(availableScenarios);

// WHEN
handler.executeScenario(httpClient, "fooBar");

// THEN
verify(httpClient).getBoschSmartHomeUrl("scenarios");
assertEquals(3, availableScenarios.size());
}

@Test
public void executeScenario_ShouldMakePostCall_IfScenarioExists() throws Exception {
// GIVEN
final var httpClient = mock(BoschHttpClient.class);
final var request = mock(Request.class);
final var response = mock(ContentResponse.class);
final Map<String, Scenario> availableScenarios = new HashMap<>();
final var testScenario = new Scenario();
testScenario.id = UUID.randomUUID().toString();
testScenario.name = "fooBar";
availableScenarios.put(testScenario.name, testScenario);
final var endpoint = String.format("scenarios/%s/triggers", testScenario.id);
when(httpClient.getBoschSmartHomeUrl(endpoint)).thenReturn(endpoint);
when(httpClient.createRequest(endpoint, HttpMethod.POST)).thenReturn(request);
when(request.send()).thenReturn(response);
when(response.getStatus()).thenReturn(200);
when(response.getContentAsString()).thenReturn("");
final var handler = new ScenarioHandler(availableScenarios);

// WHEN
handler.executeScenario(httpClient, testScenario.name);

// THEN
verify(request, times(1)).send();
}

private static Stream<Arguments> provideExceptionsForTest() {
return Stream.of(Arguments.of(new InterruptedException("call interrupted")),
Arguments.of(new TimeoutException("call timed out")),
Arguments.of(new ExecutionException(new Exception())));
}

@ParameterizedTest
@MethodSource("provideExceptionsForTest")
public void executeScenario_ShouldNotThrowException_IfApiCallsHaveException(final Exception exception)
throws Exception {
// GIVEN
final var httpClient = mock(BoschHttpClient.class);
final var request = mock(Request.class);
when(httpClient.getBoschSmartHomeUrl("scenarios")).thenReturn("http://localhost/smartHome/scenarios");
when(httpClient.createRequest(anyString(), any(HttpMethod.class))).thenReturn(request);
when(request.send()).thenThrow(exception);
final Map<String, Scenario> availableScenarios = new HashMap<>();
final var handler = new ScenarioHandler(availableScenarios);

// WHEN
handler.executeScenario(httpClient, "fooBar");

// THEN
assertTrue(availableScenarios.isEmpty());
}

@ParameterizedTest
@ValueSource(ints = { 404, 405 })
public void executeScenario_ShouldNotThrowException_IfApiCallReturnsError(final int statusCode) throws Exception {
// GIVEN
final var httpClient = mock(BoschHttpClient.class);
final var request = mock(Request.class);
final var response = mock(ContentResponse.class);
when(httpClient.getBoschSmartHomeUrl("scenarios")).thenReturn("http://localhost/smartHome/scenarios");
when(httpClient.createRequest(anyString(), any(HttpMethod.class))).thenReturn(request);
when(request.send()).thenReturn(response);
when(response.getStatus()).thenReturn(statusCode);
final Map<String, Scenario> availableScenarios = new HashMap<>();
final var handler = new ScenarioHandler(availableScenarios);

// WHEN
handler.executeScenario(httpClient, "fooBar");

// THEN
assertTrue(availableScenarios.isEmpty());
}

@Test
public void executeScenario_ShouldNotThrowException_IfResponseIsNoJson() throws Exception {
// GIVEN
final var httpClient = mock(BoschHttpClient.class);
final var request = mock(Request.class);
final var response = mock(ContentResponse.class);
when(httpClient.getBoschSmartHomeUrl("scenarios")).thenReturn("http://localhost/smartHome/scenarios");
when(httpClient.createRequest(anyString(), any(HttpMethod.class))).thenReturn(request);
when(request.send()).thenReturn(response);
when(response.getStatus()).thenReturn(200);
when(response.getContentAsString()).thenReturn("this is not a valid json");

final Map<String, Scenario> availableScenarios = new HashMap<>();
final var handler = new ScenarioHandler(availableScenarios);

// WHEN
handler.executeScenario(httpClient, "fooBar");

// THEN
assertTrue(availableScenarios.isEmpty());
}

private String getJsonStringFromFile() throws IOException {
try (InputStream input = this.getClass().getClassLoader()
.getResourceAsStream("scenarios/GET_scenarios_result.json")) {
if (input == null) {
return "";
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(input))) {
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
}
return stringBuilder.toString();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
[
{
"@type": "scenario",
"id": "c6547ee8-db0d-490a-8860-2cb90ebe59c8",
"name": "Scenario 1",
"iconId": "icon_scenario_good_night",
"actions": [
{
"deviceId": "hdm:ZigBee:cc86coffee0ad42",
"deviceServiceId": "PowerSwitch",
"targetState": {
"@type": "powerSwitchState",
"switchState": "OFF"
}
},
{
"deviceId": "hdm:HomeMaticIP:3014F711A007999878593483",
"deviceServiceId": "PowerSwitch",
"targetState": {
"@type": "powerSwitchState",
"switchState": "OFF"
}
}
]
},
{
"@type": "scenario",
"id": "509bd737-eed0-40b7-8caa-e8686a714399",
"name": "Scenario without actions",
"iconId": "icon_scenario_own_scenario",
"actions": []
},
{
"@type": "scenario",
"id": "74fcf9d6-802a-4bed-87cd-a069670f5b2a",
"name": "duplicate scenario",
"iconId": "icon_scenario_good_night",
"actions": []
},
{
"@type": "scenario",
"id": "8352bd9c-e97e-45f3-b6bb-bdf25d1ce158",
"name": "duplicate scenario",
"iconId": "icon_scenario_good_night",
"actions": []
}
]

0 comments on commit c643d58

Please sign in to comment.