Skip to content

Commit

Permalink
Merge pull request openhab#2 from coeing/feature/shutter-control
Browse files Browse the repository at this point in the history
Support for Bosch Shutter Control in-wall device
  • Loading branch information
coeing authored Jun 15, 2020
2 parents 3e70766 + 3a75c98 commit cd6ac25
Show file tree
Hide file tree
Showing 9 changed files with 360 additions and 108 deletions.
14 changes: 14 additions & 0 deletions bundles/org.openhab.binding.boschshc/.classpath
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,19 @@
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="test" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="ignore_optional_problems" value="true"/>
<attribute name="m2e-apt" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" path="target/generated-sources/annotations">
<attributes>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>
1 change: 1 addition & 0 deletions bundles/org.openhab.binding.boschshc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Binding for the Bosch Smart Home Controller:
- Bosch TwinGuard smoke detector
- Bosch Window/Door contacts
- Bosch Motion Detector
- Bosch Shutter Control in-wall

## Limitations

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
import org.eclipse.smarthome.core.thing.ThingTypeUID;

/**
* The {@link BoschSHCBindingConstants} class defines common constants, which are
* used across the whole binding.
* The {@link BoschSHCBindingConstants} class defines common constants, which
* are used across the whole binding.
*
* @author Stefan Kästle - Initial contribution
*/
Expand All @@ -33,6 +33,7 @@ public class BoschSHCBindingConstants {
public static final ThingTypeUID THING_TYPE_TWINGUARD = new ThingTypeUID(BINDING_ID, "twinguard");
public static final ThingTypeUID THING_TYPE_WINDOW_CONTACT = new ThingTypeUID(BINDING_ID, "window-contact");
public static final ThingTypeUID THING_TYPE_MOTION_DETECTOR = new ThingTypeUID(BINDING_ID, "motion-detector");
public static final ThingTypeUID THING_TYPE_SHUTTER_CONTROL = new ThingTypeUID(BINDING_ID, "shutter-control");

// List of all Channel IDs
// Auto-generated from thing-types.xml via script, don't modify
Expand All @@ -49,5 +50,5 @@ public class BoschSHCBindingConstants {
public static final String CHANNEL_COMBINED_RATING = "combined-rating";
public static final String CHANNEL_CONTACT = "contact";
public static final String CHANNEL_LATEST_MOTION = "latest-motion";

public static final String CHANNEL_LEVEL = "level";
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.smarthome.core.thing.Bridge;
import org.eclipse.smarthome.core.thing.ChannelUID;
Expand Down Expand Up @@ -183,7 +186,8 @@ private Boolean getDevices() {

if (this.devices != null) {
for (Device d : this.devices) {
// TODO keeping these as warn for the time being, until we have a better means of listing
// TODO keeping these as warn for the time being, until we have a better means
// of listing
// devices with their Bosch ID
logger.warn("Found device: name={} id={}", d.name, d.id);
if (d.deviceSerivceIDs != null) {
Expand Down Expand Up @@ -224,7 +228,8 @@ private void subscribe() {
Gson gson = new Gson();
String str_content = gson.toJson(r);

// XXX Maybe we should use a different httpClient here, to avoid a race with concurrent use from other
// XXX Maybe we should use a different httpClient here, to avoid a race with
// concurrent use from other
// functions.
logger.info("Subscribe: Sending content: {} - using httpClient {}", str_content, this.httpClient);

Expand All @@ -244,7 +249,8 @@ public void onComplete(@Nullable Result result) {
// content: [ [ '{"result":"e71k823d0-16","jsonrpc":"2.0"}\n' ] ]

// The key can then be used later for longPoll like this:
// body: [ [ '{"jsonrpc":"2.0","method":"RE/longPoll","params":["e71k823d0-16",20]}' ] ]
// body: [ [
// '{"jsonrpc":"2.0","method":"RE/longPoll","params":["e71k823d0-16",20]}' ] ]

byte[] responseContent = getContent();
String content = new String(responseContent);
Expand Down Expand Up @@ -273,17 +279,17 @@ public void onComplete(@Nullable Result result) {
/**
* Long polling
*
* TODO Do we need to protect against concurrent execution of this method via locks etc?
* TODO Do we need to protect against concurrent execution of this method via
* locks etc?
*
* If no subscription ID is present, this function will first try to acquire one. If that fails, it will attempt to
* retry after a small timeout.
* If no subscription ID is present, this function will first try to acquire
* one. If that fails, it will attempt to retry after a small timeout.
*
* Return whether to retry getting a new subscription and restart polling.
*/
private void longPoll() {
/*
* // TODO Change hard-coded Gateway ID
* // TODO Change hard-coded port
* // TODO Change hard-coded Gateway ID // TODO Change hard-coded port
*/

if (this.subscriptionId == null) {
Expand Down Expand Up @@ -454,8 +460,7 @@ private Boolean getRooms() {
ContentResponse contentResponse;
try {
logger.debug("Sending http request to Bosch to request rooms");
contentResponse = this.httpClient
.newRequest("https://" + config.ipAddress + ":8444/smarthome/rooms")
contentResponse = this.httpClient.newRequest("https://" + config.ipAddress + ":8444/smarthome/rooms")
.header("Content-Type", "application/json").header("Accept", "application/json").method(GET)
.send();

Expand Down Expand Up @@ -491,9 +496,9 @@ private Boolean getRooms() {
/**
* Query the Bosch Smart Home Controller for the state of the given thing.
*
* @param thing Thing to query the device state for
* @param thing Thing to query the device state for
* @param stateName Name of the state to query
* @param classOfT Class to convert the resulting JSON to
* @param classOfT Class to convert the resulting JSON to
*/

public <T extends Object> T refreshState(@NonNull Thing thing, String stateName, Class<T> classOfT) {
Expand Down Expand Up @@ -537,9 +542,38 @@ public <T extends Object> T refreshState(@NonNull Thing thing, String stateName,
return null;
}

/**
* Sends a state change for a device to the controller
*
* @param deviceId Id of device to change state for
* @param serviceName Name of service of device to change state for
* @param state New state data to set for service
*
* @return Response of request
*/
public <T extends Object> @Nullable Response putState(@NonNull String deviceId, String serviceName, T state) {
if (this.httpClient == null) {
return null;
}

// Create request
String url = this.createServiceUrl(serviceName, deviceId);
Request request = this.createRequest(url, PUT, state);

// Send request
try {
Response response = request.send();
return response;
} catch (InterruptedException | TimeoutException | ExecutionException e) {
logger.warn("HTTP request failed: {}", e);
return null;
}
}

/*
* TODO: The only place from which we currently send updates is the PowerSwitch, might have to extend this over time
* if we want to enable the alarm system etc.
* TODO: The only place from which we currently send updates is the PowerSwitch,
* might have to extend this over time if we want to enable the alarm system
* etc.
*/
public void updateSwitchState(@NonNull Thing thing, String command) {

Expand Down Expand Up @@ -584,4 +618,15 @@ public void updateSwitchState(@NonNull Thing thing, String command) {
}
}

private String createServiceUrl(String serviceName, String deviceId) {
return "https://" + config.ipAddress + ":8444/smarthome/devices/" + deviceId + "/services/" + serviceName
+ "/state";
}

private Request createRequest(String url, HttpMethod method, Object content) {
Gson gson = new Gson();
String body = gson.toJson(content);
return this.httpClient.newRequest(url).method(method).header("Content-Type", "application/json")
.content(new StringContentProvider(body));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory;
import org.eclipse.smarthome.core.thing.binding.ThingHandler;
import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory;
import org.openhab.binding.boschshc.internal.shuttercontrol.ShutterControlHandler;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -46,7 +47,8 @@ public class BoschSHCHandlerFactory extends BaseThingHandlerFactory {

// List of all supported Bosch devices.
public static final Collection<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Arrays.asList(THING_TYPE_SHC,
THING_TYPE_INWALL_SWITCH, THING_TYPE_TWINGUARD, THING_TYPE_WINDOW_CONTACT, THING_TYPE_MOTION_DETECTOR);
THING_TYPE_INWALL_SWITCH, THING_TYPE_TWINGUARD, THING_TYPE_WINDOW_CONTACT, THING_TYPE_MOTION_DETECTOR,
THING_TYPE_SHUTTER_CONTROL);

@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
Expand Down Expand Up @@ -81,6 +83,10 @@ else if (THING_TYPE_MOTION_DETECTOR.equals(thingTypeUID)) {
return new MotionDetectorHandler(thing);
}

else if (THING_TYPE_SHUTTER_CONTROL.equals(thingTypeUID)) {
return new ShutterControlHandler(thing);
}

else {
logger.warn("Failed to find handler for device: {}", thingTypeUID);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.openhab.binding.boschshc.internal.shuttercontrol;

public enum OperationState {
MOVING, STOPPED;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package org.openhab.binding.boschshc.internal.shuttercontrol;

import static org.openhab.binding.boschshc.internal.BoschSHCBindingConstants.*;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.smarthome.core.library.types.PercentType;
import org.eclipse.smarthome.core.library.types.StopMoveType;
import org.eclipse.smarthome.core.library.types.UpDownType;
import org.eclipse.smarthome.core.thing.Bridge;
import org.eclipse.smarthome.core.thing.ChannelUID;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.RefreshType;
import org.openhab.binding.boschshc.internal.BoschSHCBridgeHandler;
import org.openhab.binding.boschshc.internal.BoschSHCHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonSyntaxException;

/**
* Utility functions to convert data between Bosch things and openHAB items
*/
final class DataConversion {
public static int levelToOpenPercentage(double level) {
return (int) Math.round((1 - level) * 100);
}

public static double openPercentageToLevel(double openPercentage) {
return (100 - openPercentage) / 100.0;
}
}

/**
* Handler for a shutter control device
*/
public class ShutterControlHandler extends BoschSHCHandler {
private final Logger logger = LoggerFactory.getLogger(BoschSHCHandler.class);

final String ShutterControlServiceName = "ShutterControl";

public ShutterControlHandler(Thing thing) {
super(thing);
}

@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
ShutterControlState state = this.getDeviceState();
if (state == null) {
logger.warn("Could not fetch device state, skipping refresh");
return;
}
this.updateState(state);
} else if (command instanceof UpDownType) {
// Set full close/open as target state
UpDownType upDownType = (UpDownType) command;
ShutterControlState state = new ShutterControlState();
if (upDownType == UpDownType.UP) {
state.level = 1;
} else if (upDownType == UpDownType.DOWN) {
state.level = 0;
} else {
logger.warn("Received unknown UpDownType command: {}", upDownType);
return;
}
this.setDeviceState(state);
} else if (command instanceof StopMoveType) {
// Set STOPPED operation state
ShutterControlState state = new ShutterControlState();
state.operationState = OperationState.STOPPED;
this.setDeviceState(state);
} else if (command instanceof PercentType) {
// Set specific level
PercentType percentType = (PercentType) command;
double level = DataConversion.openPercentageToLevel(percentType.doubleValue());
this.setDeviceState(new ShutterControlState(level));
}
}

@Override
public void processUpdate(String id, @NonNull JsonElement state) {
try {
Gson gson = new Gson();
updateState(gson.fromJson(state, ShutterControlState.class));
} catch (JsonSyntaxException e) {
logger.warn("Received unknown update in Shutter Control: {}", state);
}
}

private BoschSHCBridgeHandler getBridgeHandler() {
Bridge bridge = this.getBridge();
if (bridge == null) {
return null;
}
return (BoschSHCBridgeHandler) bridge.getHandler();
}

private @Nullable ShutterControlState getDeviceState() {
BoschSHCBridgeHandler bridgeHandler = this.getBridgeHandler();
if (bridgeHandler == null) {
return null;
}
return bridgeHandler.refreshState(getThing(), ShutterControlServiceName, ShutterControlState.class);
}

private void setDeviceState(ShutterControlState state) {
BoschSHCBridgeHandler bridgeHandler = this.getBridgeHandler();
if (bridgeHandler == null) {
return;
}
String deviceId = this.getBoschID();
if (deviceId == null) {
return;
}
bridgeHandler.putState(deviceId, ShutterControlServiceName, state);
}

private void updateState(@NonNull ShutterControlState state) {
// Convert level to open ratio
int openPercentage = DataConversion.levelToOpenPercentage(state.level);
updateState(CHANNEL_LEVEL, new PercentType(openPercentage));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.openhab.binding.boschshc.internal.shuttercontrol;

import com.google.gson.annotations.SerializedName;

public class ShutterControlState {
@SerializedName("@type")
public String type = "shutterControlState";

/**
* Current open ratio of shutter (0.0 [closed] to 1.0 [open])
*/
public double level;

/**
* Current operation state of shutter
*/
public OperationState operationState;

public ShutterControlState() {
this.level = 0.0;
}

public ShutterControlState(double level) {
this.level = level;
}
}
Loading

0 comments on commit cd6ac25

Please sign in to comment.