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

Feature/shutter control #2

Merged
merged 8 commits into from
Jun 15, 2020
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">
coeing marked this conversation as resolved.
Show resolved Hide resolved
<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>
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,122 @@
package org.openhab.binding.boschshc.internal.shuttercontrol;

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

import org.eclipse.jdt.annotation.NonNull;
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();
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 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(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;
}
}