Skip to content

Commit

Permalink
feat: support Bosch SHC Scenarios
Browse files Browse the repository at this point in the history
Signed-off-by: Patrick Gell <[email protected]>
  • Loading branch information
pat-git023 committed Oct 14, 2023
1 parent 2d92fda commit cd078d9
Show file tree
Hide file tree
Showing 16 changed files with 587 additions and 5 deletions.
7 changes: 7 additions & 0 deletions bundles/org.openhab.binding.boschshc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,13 @@ This certificate is used for pairing between the Bridge and the Bosch Smart Home

_Press and hold the Bosch Smart Home Controller Bridge button until the LED starts blinking after you save your settings for pairing_.

### Supported Channels

| Channel Type ID | Item Type | Writable | Description |
|--------------------| -------------------- |:--------:|--------------------------------------------------------------------|
| triggered-scenario | String | &#9744; | Name of the triggered scenario (e.g. by the Universal Switch Flex) |


## Getting the device IDs

Bosch IDs for found devices are displayed in the openHAB log on bootup (`OPENHAB_FOLDER/userdata/logs/openhab.log`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
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 All @@ -34,19 +35,23 @@
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;
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;
Expand Down Expand Up @@ -99,8 +104,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 @@ -195,6 +203,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 Expand Up @@ -410,8 +422,15 @@ public boolean unregisterDiscoveryListener() {
* @param result Results from Long Polling
*/
private void handleLongPollResult(LongPollResult result) {
for (DeviceServiceData deviceServiceData : result.result) {
handleDeviceServiceData(deviceServiceData);
for (BoschSHCServiceState serviceState : result.result) {
if (DeviceServiceData.class == serviceState.getClass()) {
handleDeviceServiceData((DeviceServiceData) serviceState);
} else if (Scenario.class == serviceState.getClass()) {
final Channel channel = this.getThing().getChannel(BoschSHCBindingConstants.CHANNEL_SCENARIO);
if (channel != null && isLinked(channel.getUID())) {
updateState(channel.getUID(), new StringType(((Scenario) serviceState).name));
}
}
}
}

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 @@ -14,6 +14,8 @@

import java.util.ArrayList;

import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;

/**
* Response of the Controller for a Long Poll API call.
*
Expand All @@ -35,6 +37,6 @@ public class LongPollResult {
* ],"jsonrpc":"2.0"}
*/

public ArrayList<DeviceServiceData> result;
public ArrayList<BoschSHCServiceState> result;
public String jsonrpc;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* 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 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");
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,6 +36,7 @@ private 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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.label = Triggered Scenario
channel-type.boschshc.scenario.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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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="triggered-scenario" typeId="triggered-scenario"/>
<channel id="execute-scenario" typeId="execute-scenario"/>
</channels>

<properties>
<property name="thingTypeVersion">1</property>
</properties>

<config-description-ref uri="thing-type:boschshc:bridge"/>
</bridge-type>

Expand Down Expand Up @@ -520,4 +529,16 @@
</state>
</channel-type>

<channel-type id="triggered-scenario">
<item-type>String</item-type>
<label>Triggered Scenario</label>
<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>
Loading

0 comments on commit cd078d9

Please sign in to comment.