Skip to content

Commit

Permalink
[boschshc] Add scenario channel (openhab#15752)
Browse files Browse the repository at this point in the history
Signed-off-by: Patrick Gell <[email protected]>
  • Loading branch information
pat-git023 authored and andrewfg committed Nov 26, 2023
1 parent 4560ec9 commit c5945f0
Show file tree
Hide file tree
Showing 15 changed files with 609 additions and 5 deletions.
11 changes: 11 additions & 0 deletions bundles/org.openhab.binding.boschshc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.
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-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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,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 All @@ -55,6 +59,7 @@
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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -195,6 +203,11 @@ public void dispose() {
@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());
}
}

/**
Expand Down Expand Up @@ -410,8 +423,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 (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));
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
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,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();
}
}
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
Loading

0 comments on commit c5945f0

Please sign in to comment.