-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Patrick Gell <[email protected]> Signed-off-by: Patrick Gell <[email protected]>
- Loading branch information
1 parent
94cf11f
commit c643d58
Showing
6 changed files
with
350 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
...c/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/ScenarioHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ""; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
187 changes: 187 additions & 0 deletions
187
...c/test/java/org/openhab/binding/boschshc/internal/devices/bridge/ScenarioHandlerTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
} | ||
} |
47 changes: 47 additions & 0 deletions
47
bundles/org.openhab.binding.boschshc/src/test/resources/scenarios/GET_scenarios_result.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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": [] | ||
} | ||
] |