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 2 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
8 changes: 8 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,14 @@ 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 ID | Item Type | Writable | Description |
|--------------------|--------|:--------:|-------------------------------------------------------------------------|
david-pace marked this conversation as resolved.
Show resolved Hide resolved
| scenario-triggered | String | ☐ | Name of the triggered scenario (e.g. by the Universal Switch Flex) |
| trigger-scenario | String | ☐ | Scenario name that will be triggered on the Bosch Smart Home Controller |
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-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 httpClient = this.httpClient;
if (BoschSHCBindingConstants.CHANNEL_TRIGGER_SCENARIO.equals(channelUID.getId())
&& !RefreshType.REFRESH.equals(command) && httpClient != null) {
scenarioHandler.triggerScenario(httpClient, 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,94 @@
/**
* 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
} 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
} 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