Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[boschshc] Add scenario channel #15752

Merged
merged 6 commits into from
Nov 11, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 | ☐ | Name of the triggered scenario (e.g. by the Universal Switch Flex) |
| trigger-scenario | String | ☑ | 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,96 @@
/**
* 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 {
logger.debug("Scenario '{}' could not be found on the Bosch Smart Home Controller.", scenarioName);
}
}

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);
david-pace marked this conversation as resolved.
Show resolved Hide resolved
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();
david-pace marked this conversation as resolved.
Show resolved Hide resolved
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);
david-pace marked this conversation as resolved.
Show resolved Hide resolved
Thread.currentThread().interrupt();
} catch (TimeoutException e) {
logger.debug("Scenario call timed out", e);
} catch (ExecutionException e) {
logger.debug("Exception occurred during scenario call", e);
}
}
}
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,54 @@
/**
* 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));
}
}
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> {
david-pace marked this conversation as resolved.
Show resolved Hide resolved

@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
david-pace marked this conversation as resolved.
Show resolved Hide resolved
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-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.description = Desired temperature.
channel-type.boschshc.silent-mode.label = Silent Mode
Expand All @@ -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.label = 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.description = Current open ratio (0 to 100).

Expand Down
Loading