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 1 commit
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
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
david-pace marked this conversation as resolved.
Show resolved Hide resolved

| Channel Type ID | Item Type | Writable | Description |
david-pace marked this conversation as resolved.
Show resolved Hide resolved
|--------------------| -------------------- |:--------:|--------------------------------------------------------------------|
| triggered-scenario | String | ☐ | Name of the triggered scenario (e.g. by the Universal Switch Flex) |
david-pace marked this conversation as resolved.
Show resolved Hide resolved


## 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";
david-pace marked this conversation as resolved.
Show resolved Hide resolved
public static final String CHANNEL_EXECUTE_SCENARIO = "execute-scenario";
david-pace marked this conversation as resolved.
Show resolved Hide resolved
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;
david-pace marked this conversation as resolved.
Show resolved Hide resolved

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)
david-pace marked this conversation as resolved.
Show resolved Hide resolved
&& !command.toString().equals("REFRESH") && this.httpClient != null) {
david-pace marked this conversation as resolved.
Show resolved Hide resolved
this.scenarioHandler.executeScenario(this.httpClient, command.toString());
david-pace marked this conversation as resolved.
Show resolved Hide resolved
david-pace marked this conversation as resolved.
Show resolved Hide resolved
}
}

/**
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);
david-pace marked this conversation as resolved.
Show resolved Hide resolved
} 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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} 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));
} else if (serviceState instanceof Scenario scenario) {
final Channel channel = this.getThing().getChannel(BoschSHCBindingConstants.CHANNEL_SCENARIO);
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,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) {
david-pace marked this conversation as resolved.
Show resolved Hide resolved
this.availableScenarios = Objects.requireNonNullElseGet(availableScenarios, HashMap::new);
}

public void executeScenario(final @Nullable BoschHttpClient httpClient, final String scenarioName) {
assert httpClient != null;
david-pace marked this conversation as resolved.
Show resolved Hide resolved
if (!availableScenarios.containsKey(scenarioName)) {
david-pace marked this conversation as resolved.
Show resolved Hide resolved
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);
david-pace marked this conversation as resolved.
Show resolved Hide resolved
}
}

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);
david-pace marked this conversation as resolved.
Show resolved Hide resolved
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();
david-pace marked this conversation as resolved.
Show resolved Hide resolved
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> {
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.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