[boschshc] Initial contribution - Bindings for Bosch Smart Home devices (#8629)

* Initial code from create_openhab_binding_skeleton.sh

Signed-off-by: Stefan Kaestle <stefan@mad-kow.de>
Signed-off-by: Christian Oeing <christian.oeing@slashgames.org>
Signed-off-by: Gerd Zanker <gerd.zanker@web.de>

Co-authored-by: Stefan Kaestle <stefan@mad-kow.de>
Co-authored-by: Gerd Zanker <gerd.zanker@web.de>
Co-authored-by: Christian Oeing <christian.oeing@scalamat.de>
Co-authored-by: Hilbrand Bouwkamp <hilbrand@h72.nl>
Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
Co-authored-by: Connor Petty <mistercpp2000+gitsignoff@gmail.com>
This commit is contained in:
Christian Oeing
2021-01-17 22:20:20 +01:00
committed by GitHub
parent 2afb06948a
commit 2a5bdf3b47
65 changed files with 4462 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.boschshc-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-boschshc" description="BoschSHC Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.boschshc/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2021 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link BoschSHCBindingConstants} class defines common constants, which
* are used across the whole binding.
*
* @author Stefan Kästle - Initial contribution
* @author Christian Oeing - added Shutter Control, ThermostatHandler
*/
@NonNullByDefault
public class BoschSHCBindingConstants {
private static final String BINDING_ID = "boschshc";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_SHC = new ThingTypeUID(BINDING_ID, "shc");
public static final ThingTypeUID THING_TYPE_INWALL_SWITCH = new ThingTypeUID(BINDING_ID, "in-wall-switch");
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");
public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat");
public static final ThingTypeUID THING_TYPE_CLIMATE_CONTROL = new ThingTypeUID(BINDING_ID, "climate-control");
// List of all Channel IDs
// Auto-generated from thing-types.xml via script, don't modify
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";
public static final String CHANNEL_HUMIDITY = "humidity";
public static final String CHANNEL_HUMIDITY_RATING = "humidity-rating";
public static final String CHANNEL_ENERGY_CONSUMPTION = "energy-consumption";
public static final String CHANNEL_POWER_CONSUMPTION = "power-consumption";
public static final String CHANNEL_PURITY = "purity";
public static final String CHANNEL_AIR_DESCRIPTION = "air-description";
public static final String CHANNEL_PURITY_RATING = "purity-rating";
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";
public static final String CHANNEL_VALVE_TAPPET_POSITION = "valve-tappet-position";
public static final String CHANNEL_SETPOINT_TEMPERATURE = "setpoint-temperature";
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2021 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link BoschSHCConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Stefan Kästle - Initial contribution
*/
@NonNullByDefault
public class BoschSHCConfiguration {
/**
* ID of the device as returned by the controller.
*/
public @Nullable String id;
}

View File

@@ -0,0 +1,299 @@
/**
* Copyright (c) 2010-2021 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;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.boschshc.internal.devices.bridge.BoschSHCBridgeHandler;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.BoschSHCService;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
/**
* The {@link BoschSHCHandler} represents Bosch Things. Each type of device
* inherits from this abstract thing handler.
*
* @author Stefan Kästle - Initial contribution
* @author Christian Oeing - refactorings of e.g. server registration
*/
@NonNullByDefault
public abstract class BoschSHCHandler extends BaseThingHandler {
/**
* Service State for a Bosch device.
*/
class DeviceService<TState extends BoschSHCServiceState> {
/**
* Constructor.
*
* @param service Service which belongs to the device.
* @param affectedChannels Channels which are affected by the state of this service.
*/
public DeviceService(BoschSHCService<TState> service, Collection<String> affectedChannels) {
this.service = service;
this.affectedChannels = affectedChannels;
}
/**
* Service which belongs to the device.
*/
public final BoschSHCService<TState> service;
/**
* Channels which are affected by the state of this service.
*/
public final Collection<String> affectedChannels;
}
/**
* Reusable gson instance to convert a class to json string and back in derived classes.
*/
protected static final Gson GSON = new Gson();
protected final Logger logger = LoggerFactory.getLogger(getClass());
/**
* Bosch SHC configuration loaded from openHAB configuration.
*/
private @Nullable BoschSHCConfiguration config;
/**
* Services of the device.
*/
private List<DeviceService<? extends BoschSHCServiceState>> services = new ArrayList<>();
public BoschSHCHandler(Thing thing) {
super(thing);
}
/**
* Returns the unique id of the Bosch device.
*
* @return Unique id of the Bosch device.
*/
public @Nullable String getBoschID() {
BoschSHCConfiguration config = this.config;
if (config != null) {
return config.id;
} else {
return null;
}
}
/**
* Initializes this handler. Use this method to register all services of the device with
* {@link #registerService(BoschSHCService)}.
*/
@Override
public void initialize() {
this.config = getConfigAs(BoschSHCConfiguration.class);
try {
this.initializeServices();
// Mark immediately as online - if the bridge is online, the thing is too.
this.updateStatus(ThingStatus.ONLINE);
} catch (BoschSHCException e) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
}
}
/**
* Handles the refresh command of all registered services. Override it to handle custom commands (e.g. to update
* states of services).
*
* @param channelUID {@link ChannelUID} of the channel to which the command was sent
* @param command {@link Command}
*/
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
// Refresh state of services that affect the channel
for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
if (deviceService.affectedChannels.contains(channelUID.getIdWithoutGroup())) {
try {
deviceService.service.refreshState();
} catch (InterruptedException | TimeoutException | ExecutionException | BoschSHCException e) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
String.format("Error when trying to refresh state from service %s: %s",
deviceService.service.getServiceName(), e.getMessage()));
}
}
}
}
}
/**
* Processes an update which is received from the bridge.
*
* @param serviceName Name of service the update came from.
* @param stateData Current state of device service. Serialized as JSON.
*/
public void processUpdate(String serviceName, JsonElement stateData) {
// Check services of device to correctly
for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
BoschSHCService<? extends BoschSHCServiceState> service = deviceService.service;
if (serviceName.equals(service.getServiceName())) {
service.onStateUpdate(stateData);
}
}
}
/**
* Should be used by handlers to create their required services.
*/
protected void initializeServices() throws BoschSHCException {
}
/**
* Returns the bridge handler for this thing handler.
*
* @return Bridge handler for this thing handler. Null if no or an invalid bridge was set in the configuration.
* @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set.
*/
protected BoschSHCBridgeHandler getBridgeHandler() throws BoschSHCException {
Bridge bridge = this.getBridge();
if (bridge == null) {
throw new BoschSHCException(String.format("No valid bridge set for %s", this.getThing()));
}
BoschSHCBridgeHandler bridgeHandler = (BoschSHCBridgeHandler) bridge.getHandler();
if (bridgeHandler == null) {
throw new BoschSHCException(String.format("Bridge of %s has no valid bridge handler", this.getThing()));
}
return bridgeHandler;
}
/**
* Query the Bosch Smart Home Controller for the state of the service with the specified name.
*
* @note Use services instead of directly requesting a state.
*
* @param stateName Name of the service to query
* @param classOfT Class to convert the resulting JSON to
*/
protected <T extends BoschSHCServiceState> @Nullable T getState(String stateName, Class<T> classOfT) {
String deviceId = this.getBoschID();
if (deviceId == null) {
return null;
}
try {
BoschSHCBridgeHandler bridgeHandler = this.getBridgeHandler();
return bridgeHandler.getState(deviceId, stateName, classOfT);
} catch (InterruptedException | TimeoutException | ExecutionException | BoschSHCException e) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
String.format("Error when trying to refresh state from service %s: %s", stateName, e.getMessage()));
return null;
}
}
/**
* Creates and registers a new service for this device.
*
* @param <TService> Type of service.
* @param <TState> Type of service state.
* @param newService Supplier function to create a new instance of the service.
* @param stateUpdateListener Function to call when a state update was received
* from the device.
* @param affectedChannels Channels which are affected by the state of this
* service.
* @return Instance of registered service.
* @throws BoschSHCException
*/
protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> TService createService(
Supplier<TService> newService, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels)
throws BoschSHCException {
TService service = newService.get();
this.registerService(service, stateUpdateListener, affectedChannels);
return service;
}
/**
* Registers a service for this device.
*
* @param <TService> Type of service.
* @param <TState> Type of service state.
* @param service Service to register.
* @param stateUpdateListener Function to call when a state update was received
* from the device.
* @param affectedChannels Channels which are affected by the state of this
* service.
* @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set.
* @throws BoschSHCException If no device id is set.
*/
protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void registerService(
TService service, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels)
throws BoschSHCException {
BoschSHCBridgeHandler bridgeHandler = this.getBridgeHandler();
String deviceId = this.getBoschID();
if (deviceId == null) {
throw new BoschSHCException(
String.format("Could not register service for %s, no device id set", this.getThing()));
}
service.initialize(bridgeHandler, deviceId, stateUpdateListener);
this.registerService(service, affectedChannels);
}
/**
* Updates the state of a device service.
* Sets the status of the device to offline if setting the state fails.
*
* @param <TService> Type of service.
* @param <TState> Type of service state.
* @param service Service to set state for.
* @param state State to set.
*/
protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void updateServiceState(
TService service, TState state) {
try {
service.setState(state);
} catch (InterruptedException | TimeoutException | ExecutionException e) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String.format(
"Error when trying to update state for service %s: %s", service.getServiceName(), e.getMessage()));
}
}
/**
* Registers a service of this device.
*
* @param service Service which belongs to this device
* @param affectedChannels Channels which are affected by the state of this
* service
*/
private <TState extends BoschSHCServiceState> void registerService(BoschSHCService<TState> service,
Collection<String> affectedChannels) {
this.services.add(new DeviceService<TState>(service, affectedChannels));
}
}

View File

@@ -0,0 +1,91 @@
/**
* Copyright (c) 2010-2021 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;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_CLIMATE_CONTROL;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_MOTION_DETECTOR;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SHC;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_THERMOSTAT;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_TWINGUARD;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.boschshc.internal.devices.bridge.BoschSHCBridgeHandler;
import org.openhab.binding.boschshc.internal.devices.climatecontrol.ClimateControlHandler;
import org.openhab.binding.boschshc.internal.devices.inwallswitch.BoschInWallSwitchHandler;
import org.openhab.binding.boschshc.internal.devices.motiondetector.MotionDetectorHandler;
import org.openhab.binding.boschshc.internal.devices.shuttercontrol.ShutterControlHandler;
import org.openhab.binding.boschshc.internal.devices.thermostat.ThermostatHandler;
import org.openhab.binding.boschshc.internal.devices.twinguard.BoschTwinguardHandler;
import org.openhab.binding.boschshc.internal.devices.windowcontact.WindowContactHandler;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
/**
* The {@link BoschSHCHandlerFactory} is responsible for creating things and
* thing handlers.
*
* @author Stefan Kästle - Initial contribution
* @author Christian Oeing - Added Shutter Control and ThermostatHandler; refactored handler mapping
*/
@NonNullByDefault
@Component(configurationPid = "binding.boschshc", service = ThingHandlerFactory.class)
public class BoschSHCHandlerFactory extends BaseThingHandlerFactory {
private static class ThingTypeHandlerMapping {
public ThingTypeUID thingTypeUID;
public Function<Thing, BaseThingHandler> handlerSupplier;
public ThingTypeHandlerMapping(ThingTypeUID thingTypeUID, Function<Thing, BaseThingHandler> handlerSupplier) {
this.thingTypeUID = thingTypeUID;
this.handlerSupplier = handlerSupplier;
}
}
private static final Collection<ThingTypeHandlerMapping> SUPPORTED_THING_TYPES = List.of(
new ThingTypeHandlerMapping(THING_TYPE_SHC, thing -> new BoschSHCBridgeHandler((Bridge) thing)),
new ThingTypeHandlerMapping(THING_TYPE_INWALL_SWITCH, BoschInWallSwitchHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_TWINGUARD, BoschTwinguardHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_WINDOW_CONTACT, WindowContactHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_MOTION_DETECTOR, MotionDetectorHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_SHUTTER_CONTROL, ShutterControlHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_THERMOSTAT, ThermostatHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_CLIMATE_CONTROL, ClimateControlHandler::new));
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES.stream().anyMatch(mapping -> mapping.thingTypeUID.equals(thingTypeUID));
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
// Search for mapping for thing type and return handler for it if found. Otherwise return null.
return SUPPORTED_THING_TYPES.stream().filter(mapping -> mapping.thingTypeUID.equals(thingTypeUID)).findFirst()
.<@Nullable BaseThingHandler> map(mapping -> mapping.handlerSupplier.apply(thing)).orElse(null);
}
}

View File

@@ -0,0 +1,247 @@
/**
* Copyright (c) 2010-2021 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 static org.eclipse.jetty.http.HttpMethod.GET;
import java.nio.charset.StandardCharsets;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
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.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
/**
* HTTP client using own context with private & Bosch Certs
* to pair and connect to the Bosch Smart Home Controller.
*
* @author Gerd Zanker - Initial contribution
*/
@NonNullByDefault
public class BoschHttpClient extends HttpClient {
private static final Gson GSON = new Gson();
private final Logger logger = LoggerFactory.getLogger(BoschHttpClient.class);
private final String ipAddress;
private final String systemPassword;
public BoschHttpClient(String ipAddress, String systemPassword, SslContextFactory sslContextFactory) {
super(sslContextFactory);
this.ipAddress = ipAddress;
this.systemPassword = systemPassword;
}
/**
* Returns the pairing URL for the Bosch SHC clients, using port 8443.
* See https://github.com/BoschSmartHome/bosch-shc-api-docs/blob/master/postman/README.md
*
* @return URL for pairing
*/
public String getPairingUrl() {
return String.format("https://%s:8443/smarthome/clients", this.ipAddress);
}
/**
* Returns a Bosch SHC URL for the endpoint, using port 8444.
*
* @param endpoint a endpoint, see https://apidocs.bosch-smarthome.com/local/index.html
* @return Bosch SHC URL for passed endpoint
*/
public String getBoschShcUrl(String endpoint) {
return String.format("https://%s:8444/%s", this.ipAddress, endpoint);
}
/**
* Returns a SmartHome URL for the endpoint - shortcut of {@link BoschSslUtil::getBoschShcUrl()}
*
* @param endpoint a endpoint, see https://apidocs.bosch-smarthome.com/local/index.html
* @return SmartHome URL for passed endpoint
*/
public String getBoschSmartHomeUrl(String endpoint) {
return this.getBoschShcUrl(String.format("smarthome/%s", endpoint));
}
/**
* Returns a device & service URL.
* see https://apidocs.bosch-smarthome.com/local/index.html
*
* @param serviceName the name of the service
* @param deviceId the device identifier
* @return SmartHome URL for passed endpoint
*/
public String getServiceUrl(String serviceName, String deviceId) {
return this.getBoschSmartHomeUrl(String.format("devices/%s/services/%s/state", deviceId, serviceName));
}
/**
* Checks if the Bosch SHC can be accessed.
*
* @return true if HTTP access was successful
* @throws InterruptedException in case of an interrupt
*/
public boolean isAccessPossible() throws InterruptedException {
try {
String url = this.getBoschSmartHomeUrl("devices");
Request request = this.createRequest(url, GET);
ContentResponse contentResponse = request.send();
String content = contentResponse.getContentAsString();
logger.debug("Access check response complete: {} - return code: {}", content, contentResponse.getStatus());
return true;
} catch (TimeoutException | ExecutionException | NullPointerException e) {
logger.debug("Access check response failed because of {}!", e.getMessage());
return false;
}
}
/**
* Pairs this client with the Bosch SHC.
* Press pairing button on the Bosch Smart Home Controller!
*
* @return true if pairing was successful, otherwise false
* @throws InterruptedException in case of an interrupt
*/
public boolean doPairing() throws InterruptedException {
logger.trace("Starting pairing openHAB Client with Bosch SmartHomeController!");
logger.trace("Please press the Bosch SHC button until LED starts blinking");
ContentResponse contentResponse;
try {
String publicCert = getCertFromSslContextFactory();
logger.trace("Pairing with SHC {}", ipAddress);
// JSON Rest content
Map<String, String> items = new HashMap<>();
items.put("@type", "client");
items.put("id", BoschSslUtil.getBoschShcClientId()); // Client Id contains the unique OpenHab instance Id
items.put("name", "oss_OpenHAB_Binding"); // Client name according to
// https://github.com/BoschSmartHome/bosch-shc-api-docs#terms-and-conditions
items.put("primaryRole", "ROLE_RESTRICTED_CLIENT");
items.put("certificate", "-----BEGIN CERTIFICATE-----\r" + publicCert + "\r-----END CERTIFICATE-----");
String url = this.getPairingUrl();
Request request = this.createRequest(url, HttpMethod.POST, items).header("Systempassword",
Base64.getEncoder().encodeToString(this.systemPassword.getBytes(StandardCharsets.UTF_8)));
contentResponse = request.send();
logger.trace("Pairing response complete: {} - return code: {}", contentResponse.getContentAsString(),
contentResponse.getStatus());
if (201 == contentResponse.getStatus()) {
logger.debug("Pairing successful.");
return true;
} else {
logger.info("Pairing failed with response status {}.", contentResponse.getStatus());
return false;
}
} catch (TimeoutException | CertificateEncodingException | KeyStoreException | NullPointerException e) {
logger.warn("Pairing failed with exception {}", e.getMessage());
return false;
} catch (ExecutionException e) {
// javax.net.ssl.SSLHandshakeException: General SSLEngine problem
// => usually the pairing failed, because hardware button was not pressed.
logger.trace("Pairing failed - Details: {}", e.getMessage());
logger.warn("Pairing failed. Was the Bosch SHC button pressed?");
return false;
}
}
/**
* Creates a HTTP request.
*
* @param url for the HTTP request
* @param method for the HTTP request
* @return created HTTP request instance
*/
public Request createRequest(String url, HttpMethod method) {
return this.createRequest(url, method, null);
}
/**
* Creates a HTTP request.
*
* @param url for the HTTP request
* @param method for the HTTP request
* @param content for the HTTP request
* @return created HTTP request instance
*/
public Request createRequest(String url, HttpMethod method, @Nullable Object content) {
Request request = this.newRequest(url).method(method).header("Content-Type", "application/json");
if (content != null) {
String body = GSON.toJson(content);
logger.trace("create request for {} and content {}", url, body);
request = request.content(new StringContentProvider(body));
} else {
logger.trace("create request for {}", url);
}
// Set default timeout
request.timeout(10, TimeUnit.SECONDS);
return request;
}
/**
* Sends a request and expects a response of the specified type.
*
* @param request Request to send
* @param responseContentClass Type of expected response
* @throws ExecutionException in case of invalid HTTP request result
* @throws TimeoutException in case of an HTTP request timeout
* @throws InterruptedException in case of an interrupt
*/
public <TContent> TContent sendRequest(Request request, Class<TContent> responseContentClass)
throws InterruptedException, TimeoutException, ExecutionException {
ContentResponse contentResponse = request.send();
logger.debug("BoschHttpClient: response complete: {} - return code: {}", contentResponse.getContentAsString(),
contentResponse.getStatus());
try {
@Nullable
TContent content = GSON.fromJson(contentResponse.getContentAsString(), responseContentClass);
if (content == null) {
throw new ExecutionException(String.format("Received no content in response, expected type %s",
responseContentClass.getName()), null);
}
return content;
} catch (JsonSyntaxException e) {
throw new ExecutionException(String.format("Received invalid content in response, expected type %s: %s",
responseContentClass.getName(), e.getMessage()), e);
}
}
private String getCertFromSslContextFactory() throws KeyStoreException, CertificateEncodingException {
Certificate cert = this.getSslContextFactory().getKeyStore()
.getCertificate(BoschSslUtil.getBoschShcServerId(ipAddress));
return Base64.getEncoder().encodeToString(cert.getEncoded());
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2021 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 org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link BoschSHCBridgeConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Stefan Kästle - Initial contribution
*/
@NonNullByDefault
public class BoschSHCBridgeConfiguration {
/**
* IP address of the Bosch Smart Home Controller
*/
public String ipAddress = "";
/**
* Password of the Bosch Smart Home Controller. Set during initialization via the Bosch app.
*/
public String password = "";
}

View File

@@ -0,0 +1,410 @@
/**
* Copyright (c) 2010-2021 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 static org.eclipse.jetty.http.HttpMethod.GET;
import static org.eclipse.jetty.http.HttpMethod.PUT;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
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.client.api.Response;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.*;
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.services.dto.BoschSHCServiceState;
import org.openhab.binding.boschshc.internal.services.dto.JsonRestExceptionResponse;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
/**
* Representation of a connection with a Bosch Smart Home Controller bridge.
*
* @author Stefan Kästle - Initial contribution
* @author Gerd Zanker - added HttpClient with pairing support
* @author Christian Oeing - refactorings of e.g. server registration
*/
@NonNullByDefault
public class BoschSHCBridgeHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(BoschSHCBridgeHandler.class);
/**
* gson instance to convert a class to json string and back.
*/
private final Gson gson = new Gson();
/**
* Handler to do long polling.
*/
private final LongPolling longPolling;
private @Nullable BoschHttpClient httpClient;
private @Nullable ScheduledFuture<?> scheduledPairing;
public BoschSHCBridgeHandler(Bridge bridge) {
super(bridge);
this.longPolling = new LongPolling(this.scheduler, this::handleLongPollResult, this::handleLongPollFailure);
}
@Override
public void initialize() {
// Read configuration
BoschSHCBridgeConfiguration config = getConfigAs(BoschSHCBridgeConfiguration.class);
if (config.ipAddress.isEmpty()) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No IP address set");
return;
}
if (config.password.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No system password set");
return;
}
SslContextFactory factory;
try {
// prepare SSL key and certificates
factory = new BoschSslUtil(config.ipAddress).getSslContextFactory();
} catch (PairingFailedException e) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"@text/offline.conf-error-ssl");
return;
}
// Instantiate HttpClient with the SslContextFactory
BoschHttpClient httpClient = this.httpClient = new BoschHttpClient(config.ipAddress, config.password, factory);
// Start http client
try {
httpClient.start();
} catch (Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
String.format("Could not create http connection to controller: %s", e.getMessage()));
return;
}
// Initialize bridge in the background.
// Start initial access the first time
scheduleInitialAccess(httpClient);
}
@Override
public void dispose() {
// Cancel scheduled pairing.
ScheduledFuture<?> scheduledPairing = this.scheduledPairing;
if (scheduledPairing != null) {
scheduledPairing.cancel(true);
this.scheduledPairing = null;
}
// Stop long polling.
this.longPolling.stop();
BoschHttpClient httpClient = this.httpClient;
if (httpClient != null) {
try {
httpClient.stop();
} catch (Exception e) {
logger.debug("HttpClient failed on bridge disposal: {}", e.getMessage());
}
this.httpClient = null;
}
super.dispose();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
/**
* Schedule the initial access.
* Use a delay if pairing fails and next retry is scheduled.
*/
private void scheduleInitialAccess(BoschHttpClient httpClient) {
this.scheduledPairing = scheduler.schedule(() -> initialAccess(httpClient), 15, TimeUnit.SECONDS);
}
/**
* Execute the initial access.
* Uses the HTTP Bosch SHC client
* to check if access if possible
* pairs this Bosch SHC Bridge with the SHC if necessary
* and starts the first log poll.
*/
private void initialAccess(BoschHttpClient httpClient) {
logger.debug("Initializing Bosch SHC Bridge: {} - HTTP client is: {} - version: 2020-04-05", this, httpClient);
try {
// check access and pair if necessary
if (!httpClient.isAccessPossible()) {
// update status already if access is not possible
this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
"@text/offline.conf-error-pairing");
if (!httpClient.doPairing()) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"@text/offline.conf-error-pairing");
}
// restart initial access - needed also in case of successful pairing to check access again
scheduleInitialAccess(httpClient);
} else {
// print rooms and devices if things are reachable
boolean thingReachable = true;
thingReachable &= this.getRooms();
thingReachable &= this.getDevices();
if (thingReachable) {
this.updateStatus(ThingStatus.ONLINE);
// Start long polling
try {
this.longPolling.start(httpClient);
} catch (LongPollingFailedException e) {
this.handleLongPollFailure(e);
}
} else {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"@text/offline.not-reachable");
// restart initial access
scheduleInitialAccess(httpClient);
}
}
} catch (InterruptedException e) {
this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
String.format("Pairing was interrupted: %s", e.getMessage()));
}
}
/**
* Get a list of connected devices from the Smart-Home Controller
*
* @throws InterruptedException
*/
private boolean getDevices() throws InterruptedException {
BoschHttpClient httpClient = this.httpClient;
if (httpClient == null) {
return false;
}
try {
logger.debug("Sending http request to Bosch to request clients: {}", httpClient);
String url = httpClient.getBoschSmartHomeUrl("devices");
ContentResponse contentResponse = httpClient.createRequest(url, GET).send();
String content = contentResponse.getContentAsString();
logger.debug("Response complete: {} - return code: {}", content, contentResponse.getStatus());
Type collectionType = new TypeToken<ArrayList<Device>>() {
}.getType();
ArrayList<Device> devices = gson.fromJson(content, collectionType);
if (devices != null) {
for (Device d : devices) {
// Write found devices into openhab.log until we have implemented auto discovery
logger.info("Found device: name={} id={}", d.name, d.id);
if (d.deviceSerivceIDs != null) {
for (String s : d.deviceSerivceIDs) {
logger.info(".... service: {}", s);
}
}
}
}
} catch (TimeoutException | ExecutionException e) {
logger.debug("HTTP request failed with exception {}", e.getMessage());
return false;
}
return true;
}
private void handleLongPollResult(LongPollResult result) {
for (DeviceStatusUpdate update : result.result) {
if (update != null && update.state != null) {
logger.debug("Got update for {}", update.deviceId);
boolean handled = false;
Bridge bridge = this.getThing();
for (Thing childThing : bridge.getThings()) {
// All children of this should implement BoschSHCHandler
ThingHandler baseHandler = childThing.getHandler();
if (baseHandler != null && baseHandler instanceof BoschSHCHandler) {
BoschSHCHandler handler = (BoschSHCHandler) baseHandler;
String deviceId = handler.getBoschID();
handled = true;
logger.debug("Registered device: {} - looking for {}", deviceId, update.deviceId);
if (deviceId != null && update.deviceId.equals(deviceId)) {
logger.debug("Found child: {} - calling processUpdate with {}", handler, update.state);
handler.processUpdate(update.id, update.state);
}
} else {
logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler);
}
}
if (!handled) {
logger.debug("Could not find a thing for device ID: {}", update.deviceId);
}
}
}
}
private void handleLongPollFailure(Throwable e) {
logger.warn("Long polling failed", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Long polling failed");
}
/**
* Get a list of rooms from the Smart-Home controller
*
* @throws InterruptedException
*/
private boolean getRooms() throws InterruptedException {
BoschHttpClient httpClient = this.httpClient;
if (httpClient != null) {
try {
logger.debug("Sending http request to Bosch to request rooms");
String url = httpClient.getBoschSmartHomeUrl("rooms");
ContentResponse contentResponse = httpClient.createRequest(url, GET).send();
String content = contentResponse.getContentAsString();
logger.debug("Response complete: {} - return code: {}", content, contentResponse.getStatus());
Type collectionType = new TypeToken<ArrayList<Room>>() {
}.getType();
ArrayList<Room> rooms = gson.fromJson(content, collectionType);
if (rooms != null) {
for (Room r : rooms) {
logger.info("Found room: {}", r.name);
}
}
return true;
} catch (TimeoutException | ExecutionException e) {
logger.warn("HTTP request failed: {}", e.getMessage());
return false;
}
} else {
return false;
}
}
/**
* Query the Bosch Smart Home Controller for the state of the given thing.
*
* @param deviceId Id of device to get state for
* @param stateName Name of the state to query
* @param stateClass Class to convert the resulting JSON to
* @throws ExecutionException
* @throws TimeoutException
* @throws InterruptedException
* @throws BoschSHCException
*/
public <T extends BoschSHCServiceState> @Nullable T getState(String deviceId, String stateName, Class<T> stateClass)
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
BoschHttpClient httpClient = this.httpClient;
if (httpClient == null) {
logger.warn("HttpClient not initialized");
return null;
}
String url = httpClient.getServiceUrl(stateName, deviceId);
Request request = httpClient.createRequest(url, GET).header("Accept", "application/json");
logger.debug("refreshState: Requesting \"{}\" from Bosch: {} via {}", stateName, deviceId, url);
ContentResponse contentResponse = request.send();
String content = contentResponse.getContentAsString();
logger.debug("refreshState: Request complete: [{}] - return code: {}", content, contentResponse.getStatus());
int statusCode = contentResponse.getStatus();
if (statusCode != 200) {
JsonRestExceptionResponse errorResponse = gson.fromJson(content, JsonRestExceptionResponse.class);
if (errorResponse != null) {
throw new BoschSHCException(String.format(
"State request for service %s of device %s failed with status code %d and error code %s",
stateName, deviceId, errorResponse.statusCode, errorResponse.errorCode));
} else {
throw new BoschSHCException(
String.format("State request for service %s of device %s failed with status code %d", stateName,
deviceId, statusCode));
}
}
@Nullable
T state = gson.fromJson(content, stateClass);
if (state == null) {
throw new BoschSHCException(String.format("Received invalid, expected type %s", stateClass.getName()));
}
return state;
}
/**
* 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
* @throws InterruptedException
* @throws ExecutionException
* @throws TimeoutException
*/
public <T extends BoschSHCServiceState> @Nullable Response putState(String deviceId, String serviceName, T state)
throws InterruptedException, TimeoutException, ExecutionException {
BoschHttpClient httpClient = this.httpClient;
if (httpClient == null) {
logger.warn("HttpClient not initialized");
return null;
}
// Create request
String url = httpClient.getServiceUrl(serviceName, deviceId);
Request request = httpClient.createRequest(url, PUT, state);
// Send request
Response response = request.send();
return response;
}
}

View File

@@ -0,0 +1,219 @@
/**
* Copyright (c) 2010-2021 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.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.Security;
import java.security.Signature;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException;
import org.openhab.core.OpenHAB;
import org.openhab.core.id.InstanceUUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* SSL context utility.
*
* @author Gerd Zanker - Initial contribution
*/
@NonNullByDefault
public class BoschSslUtil {
private static final String OSS_OPENHAB_BINDING = "oss_openhab_binding";
private static final String KEYSTORE_PASSWORD = "openhab";
private final Logger logger = LoggerFactory.getLogger(BoschSslUtil.class);
private final String boschShcServerID;
private final String keystorePath;
/**
* Returns unique ID for this Bosch SmartHomeController client.
*
* @return unique string containing the openhab UUID.
*/
public static String getBoschShcClientId() {
return OSS_OPENHAB_BINDING + "_" + InstanceUUID.get();
}
/**
* Returns ID for passed Bosch SmartHomeController server.
*
* @param shcServerID the ip address of the SHC server
* @return unique string containing the server id
*/
public static String getBoschShcServerId(String shcServerID) {
return OSS_OPENHAB_BINDING + "_" + shcServerID;
}
/**
* Constructor
*
* @param boschShcServerID the ip address of the SHC server
*/
public BoschSslUtil(String boschShcServerID) {
this.boschShcServerID = boschShcServerID;
this.keystorePath = getKeystorePath();
}
/// Returns unique ID for Bosch SmartHomeController server.
public String getBoschShcServerId() {
return BoschSslUtil.getBoschShcServerId(boschShcServerID);
}
/// Returns the unique keystore for each Bosch Smart Home Controller server.
public String getKeystorePath() {
return Paths.get(OpenHAB.getUserDataFolder(), "etc", getBoschShcServerId() + ".jks").toString();
}
public SslContextFactory getSslContextFactory() throws PairingFailedException {
// Instantiate and configure the SslContextFactory
SslContextFactory sslContextFactory = new SslContextFactory.Client.Client(true); // Accept all certificates
// during pairing the cert from this keystore is accessed by HTTP client via name
sslContextFactory.setKeyStore(getKeyStoreAndCreateIfNecessary());
// Keystore for managing the keys that have been used to pair with the SHC
// https://www.eclipse.org/jetty/javadoc/9.4.12.v20180830/org/eclipse/jetty/util/ssl/SslContextFactory.html
sslContextFactory.setKeyStorePath(keystorePath);
sslContextFactory.setKeyStorePassword(KEYSTORE_PASSWORD);
// Bosch is using a self signed certificate
sslContextFactory.setTrustAll(true);
sslContextFactory.setValidateCerts(false);
sslContextFactory.setValidatePeerCerts(false);
sslContextFactory.setEndpointIdentificationAlgorithm(null);
return sslContextFactory;
}
public KeyStore getKeyStoreAndCreateIfNecessary() throws PairingFailedException {
try {
File file = new File(keystorePath);
if (!file.exists()) {
// create new keystore
logger.info("Creating new keystore {} because it doesn't exist.", keystorePath);
return createKeyStore(keystorePath);
} else {
// load keystore as a first check
KeyStore keyStore = KeyStore.getInstance("JKS");
try (FileInputStream keystoreStream = new FileInputStream(file)) {
keyStore.load(keystoreStream, KEYSTORE_PASSWORD.toCharArray());
}
logger.debug("Using existing keystore {}", keystorePath);
return keyStore;
}
} catch (OperatorCreationException | GeneralSecurityException | IOException e) {
logger.debug("Exception during keystore creation {}", e.getMessage());
throw new PairingFailedException("Can not create or load keystore file: " + keystorePath
+ ". Check path, write access and JKS content.", e);
}
}
private X509Certificate generateClientCertificate(KeyPair keyPair)
throws GeneralSecurityException, OperatorCreationException {
final String dirName = "CN=" + getBoschShcClientId() + ", O=openHAB, L=None, ST=None, C=None";
logger.debug("Creating a new self signed certificate: {}", dirName);
final Instant now = Instant.now();
final Date notBefore = Date.from(now);
final Date notAfter = Date.from(now.plus(Duration.ofDays(365 * 10)));
X500Name name = new X500Name(dirName);
// create the certificate
X509v3CertificateBuilder certificateBuilder = new JcaX509v3CertificateBuilder(name, // Issuer
BigInteger.valueOf(now.toEpochMilli()), notBefore, notAfter, name, // Subject
keyPair.getPublic() // Public key to be associated with the certificate
);
// and sign it
ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSA").build(keyPair.getPrivate());
return new JcaX509CertificateConverter().setProvider(new BouncyCastleProvider())
.getCertificate(certificateBuilder.build(contentSigner));
}
private KeyStore createKeyStore(String keystore)
throws IOException, OperatorCreationException, GeneralSecurityException {
// create a new keystore
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(null, null);
// create new key pair for BoschSHC binding
logger.debug("Creating new keypair");
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
KeyPair keyPair = kpg.generateKeyPair();
Security.addProvider(new BouncyCastleProvider());
Signature signer = Signature.getInstance("SHA256withRSA", "BC");
signer.initSign(keyPair.getPrivate());
signer.update("Hello openHAB".getBytes(StandardCharsets.UTF_8));
signer.sign();
X509Certificate cert = generateClientCertificate(keyPair);
logger.debug("Adding keyEntry '{}' with self signed certificate to keystore", getBoschShcServerId());
keyStore.setKeyEntry(getBoschShcServerId(), keyPair.getPrivate(), KEYSTORE_PASSWORD.toCharArray(),
new Certificate[] { cert });
// add Bosch Certs
CertificateFactory cf = CertificateFactory.getInstance("X.509");
logger.debug("Adding Issuing CA to keystore");
try (BufferedInputStream streamIssuingCA = new BufferedInputStream(
this.getClass().getResourceAsStream("SmartHomeControllerIssuingCA.pem"))) {
Certificate certIssuingCA = cf.generateCertificate(streamIssuingCA);
keyStore.setCertificateEntry("Smart Home Controller Issuing CA", certIssuingCA);
}
logger.debug("Adding root CA to keystore");
try (BufferedInputStream streamRootCa = new BufferedInputStream(
this.getClass().getResourceAsStream("SmartHomeControllerProductiveRootCA.pem"))) {
Certificate certRooCA = cf.generateCertificate(streamRootCa);
keyStore.setCertificateEntry("Smart Home Controller Productive Root CA", certRooCA);
}
logger.debug("Storing keystore to file {}", keystore);
try (FileOutputStream keystoreStream = new FileOutputStream(keystore)) {
keyStore.store(keystoreStream, KEYSTORE_PASSWORD.toCharArray());
}
return keyStore;
}
}

View File

@@ -0,0 +1,62 @@
/**
* Copyright (c) 2010-2021 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 org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Payload as POST data for triggering a RPC call on the Bosch Smart Home Controller.
*
* @author Stefan Kästle - Initial contribution
*/
@NonNullByDefault
class JsonRpcRequest {
public String jsonrpc;
public String method;
public String[] params;
public JsonRpcRequest(String jsonrpc, String method, String[] params) {
this.jsonrpc = jsonrpc;
this.method = method;
this.params = params;
}
public JsonRpcRequest() {
this("", "", new String[0]);
}
public String getJsonrpc() {
return jsonrpc;
}
public void setJsonrpc(String jsonrpc) {
this.jsonrpc = jsonrpc;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String[] getParams() {
return params;
}
public void setParams(String[] params) {
this.params = params;
}
}

View File

@@ -0,0 +1,211 @@
/**
* Copyright (c) 2010-2021 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 static org.eclipse.jetty.http.HttpMethod.POST;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollError;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* Handles the long polling to the Smart Home Controller.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public class LongPolling {
private final Logger logger = LoggerFactory.getLogger(LongPolling.class);
/**
* gson instance to convert a class to json string and back.
*/
private final Gson gson = new Gson();
/**
* Executor to schedule long polls.
*/
private final ScheduledExecutorService scheduler;
/**
* Handler for long poll results.
*/
private final Consumer<LongPollResult> handleResult;
/**
* Handler for unrecoverable.
*/
private final Consumer<Throwable> handleFailure;
/**
* Current running long polling request.
*/
private @Nullable Request request;
/**
* Indicates if long polling was aborted.
*/
private boolean aborted = false;
public LongPolling(ScheduledExecutorService scheduler, Consumer<LongPollResult> handleResult,
Consumer<Throwable> handleFailure) {
this.scheduler = scheduler;
this.handleResult = handleResult;
this.handleFailure = handleFailure;
}
public void start(BoschHttpClient httpClient) throws LongPollingFailedException {
// Subscribe to state updates.
String subscriptionId = this.subscribe(httpClient);
this.executeLongPoll(httpClient, subscriptionId);
}
public void stop() {
// Abort long polling.
this.aborted = true;
Request request = this.request;
if (request != null) {
request.abort(new AbortLongPolling());
this.request = null;
}
}
/**
* Subscribe to events and store the subscription ID needed for long polling.
*
* @param httpClient Http client to use for sending subscription request
* @return Subscription id
*/
private String subscribe(BoschHttpClient httpClient) throws LongPollingFailedException {
try {
String url = httpClient.getBoschShcUrl("remote/json-rpc");
JsonRpcRequest request = new JsonRpcRequest("2.0", "RE/subscribe",
new String[] { "com/bosch/sh/remote/*", null });
logger.debug("Subscribe: Sending request: {} - using httpClient {}", gson.toJson(request), httpClient);
Request httpRequest = httpClient.createRequest(url, POST, request);
SubscribeResult response = httpClient.sendRequest(httpRequest, SubscribeResult.class);
logger.debug("Subscribe: Got subscription ID: {} {}", response.getResult(), response.getJsonrpc());
String subscriptionId = response.getResult();
return subscriptionId;
} catch (TimeoutException | ExecutionException | InterruptedException e) {
throw new LongPollingFailedException("Error on subscribe request", e);
}
}
private void executeLongPoll(BoschHttpClient httpClient, String subscriptionId) {
scheduler.execute(() -> this.longPoll(httpClient, subscriptionId));
}
/**
* Start long polling the home controller. Once a long poll resolves, a new one is started.
*/
private void longPoll(BoschHttpClient httpClient, String subscriptionId) {
logger.debug("Sending long poll request");
JsonRpcRequest requestContent = new JsonRpcRequest("2.0", "RE/longPoll", new String[] { subscriptionId, "20" });
String url = httpClient.getBoschShcUrl("remote/json-rpc");
Request request = httpClient.createRequest(url, POST, requestContent);
// Long polling responds after 20 seconds with an empty response if no update has happened.
// 10 second threshold was added to not time out if response from controller takes a bit longer than 20 seconds.
request.timeout(30, TimeUnit.SECONDS);
this.request = request;
LongPolling longPolling = this;
request.send(new BufferingResponseListener() {
@Override
public void onComplete(@Nullable Result result) {
Throwable failure = result != null ? result.getFailure() : null;
if (failure != null) {
if (failure instanceof ExecutionException) {
if (failure.getCause() instanceof AbortLongPolling) {
logger.debug("Canceling long polling for subscription id {} because it was aborted",
subscriptionId);
} else {
longPolling.handleFailure.accept(new LongPollingFailedException(
"Unexpected exception during long polling request", failure));
}
} else {
longPolling.handleFailure.accept(new LongPollingFailedException(
"Unexpected exception during long polling request", failure));
}
} else {
longPolling.onLongPollResponse(httpClient, subscriptionId, this.getContentAsString());
}
}
});
}
private void onLongPollResponse(BoschHttpClient httpClient, String subscriptionId, String content) {
// Check if thing is still online
if (this.aborted) {
logger.debug("Canceling long polling for subscription id {} because it was aborted", subscriptionId);
return;
}
logger.debug("Long poll response: {}", content);
String nextSubscriptionId = subscriptionId;
LongPollResult longPollResult = gson.fromJson(content, LongPollResult.class);
if (longPollResult != null && longPollResult.result != null) {
this.handleResult.accept(longPollResult);
} else {
logger.warn("Long poll response contained no results: {}", content);
// Check if we got a proper result from the SHC
LongPollError longPollError = gson.fromJson(content, LongPollError.class);
if (longPollError != null && longPollError.error != null) {
logger.warn("Got long poll error: {} (code: {})", longPollError.error.message,
longPollError.error.code);
if (longPollError.error.code == LongPollError.SUBSCRIPTION_INVALID) {
logger.warn("Subscription {} became invalid, subscribing again", subscriptionId);
try {
nextSubscriptionId = this.subscribe(httpClient);
} catch (LongPollingFailedException e) {
this.handleFailure.accept(e);
return;
}
}
}
}
// Execute next run.
this.executeLongPoll(httpClient, nextSubscriptionId);
}
@SuppressWarnings("serial")
private class AbortLongPolling extends BoschSHCException {
}
}

View File

@@ -0,0 +1,57 @@
/**
* Copyright (c) 2010-2021 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.List;
import com.google.gson.annotations.SerializedName;
/**
* Represents a single devices connected to the Bosch Smart Home Controller.
*
* Example from Json:
*
* {
* "@type":"device",
* "rootDeviceId":"64-da-a0-02-14-9b",
* "id":"hdm:HomeMaticIP:3014F711A00004953859F31B",
* "deviceServiceIds":["PowerMeter","PowerSwitch","PowerSwitchProgram","Routing"],
* "manufacturer":"BOSCH",
* "roomId":"hz_3",
* "deviceModel":"PSM",
* "serial":"3014F711A00004953859F31B",
* "profile":"GENERIC",
* "name":"Coffee Machine",
* "status":"AVAILABLE",
* "childDeviceIds":[]
* }
*
* @author Stefan Kästle - Initial contribution
*/
public class Device {
@SerializedName("@type")
public String type;
public String rootDeviceId;
public String id;
public List<String> deviceSerivceIDs;
public String manufacturer;
public String roomId;
public String deviceModel;
public String serial;
public String profile;
public String name;
public String status;
public List<String> childDeviceIds;
}

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2021 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 com.google.gson.JsonElement;
import com.google.gson.annotations.SerializedName;
/**
* Represents a device status update as represented by the Smart Home
* Controller.
*
* @author Stefan Kästle - Initial contribution
* @author Christian Oeing - refactorings of e.g. server registration
*/
public class DeviceStatusUpdate {
/**
* Url path of the service the update came from.
*/
public String path;
/**
* The type of message.
*/
@SerializedName("@type")
public String type;
/**
* Name of service the update came from.
*/
public String id;
/**
* Current state of device. Serialized as JSON.
*/
public JsonElement state;
/**
* Id of device the update is for.
*/
public String deviceId;
@Override
public String toString() {
return this.deviceId + "state: " + this.type;
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2021 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;
/**
* Error response of the Controller for a Long Poll API call.
*
* @author Stefan Kästle - Initial contribution
*/
public class LongPollError {
public static final int SUBSCRIPTION_INVALID = -32001;
/**
* {
* "jsonrpc":"2.0",
* "error": {
* "code":-32001,
* "message":"No subscription with id: e8fei62b0-0"
* }
* }
*/
public class ErrorInfo {
public int code;
public String message;
}
public String jsonrpc;
public ErrorInfo error;
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2021 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.ArrayList;
/**
* Response of the Controller for a Long Poll API call.
*
* @author Stefan Kästle - Initial contribution
*/
public class LongPollResult {
/**
* {"result":[
* ..{
* ...."path":"/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch",
* ...."@type":"DeviceServiceData",
* ...."id":"PowerSwitch",
* ...."state":{
* ......"@type":"powerSwitchState",
* ......"switchState":"ON"
* ....},
* ...."deviceId":"hdm:HomeMaticIP:3014F711A0001916D859A8A9"}
* ],"jsonrpc":"2.0"}
*/
public ArrayList<DeviceStatusUpdate> result;
public String jsonrpc;
}

View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2021 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 com.google.gson.annotations.SerializedName;
/**
* A room as represented by the controller.
*
* Json example:
* {"@type":"room","id":"hz_1","iconId":"icon_room_bedroom","name":"Bedroom"}
*
* @author Stefan Kästle - Initial contribution
*/
public class Room {
@SerializedName("@type")
public String type;
public String id;
public String name;
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2021 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;
/**
* Response of the Controller for a Long Poll API call.
*
* The result field will contain the subscription ID needed for further API calls (e.g. the long polling call)
*
* @author Stefan Kästle - Initial contribution
*/
public class SubscribeResult {
private String result;
private String jsonrpc;
public String getResult() {
return this.result;
}
public String getJsonrpc() {
return this.jsonrpc;
}
}

View File

@@ -0,0 +1,106 @@
/**
* Copyright (c) 2010-2021 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.climatecontrol;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_SETPOINT_TEMPERATURE;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.roomclimatecontrol.RoomClimateControlService;
import org.openhab.binding.boschshc.internal.services.roomclimatecontrol.dto.RoomClimateControlServiceState;
import org.openhab.binding.boschshc.internal.services.temperaturelevel.TemperatureLevelService;
import org.openhab.binding.boschshc.internal.services.temperaturelevel.dto.TemperatureLevelServiceState;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
/**
* A virtual device which controls up to six Bosch Smart Home radiator thermostats in a room.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public final class ClimateControlHandler extends BoschSHCHandler {
private RoomClimateControlService roomClimateControlService;
/**
* Constructor.
*
* @param thing The Bosch Smart Home device that should be handled.
*/
public ClimateControlHandler(Thing thing) {
super(thing);
this.roomClimateControlService = new RoomClimateControlService();
}
@Override
protected void initializeServices() throws BoschSHCException {
super.createService(TemperatureLevelService::new, this::updateChannels, List.of(CHANNEL_TEMPERATURE));
super.registerService(this.roomClimateControlService, this::updateChannels,
List.of(CHANNEL_SETPOINT_TEMPERATURE));
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
switch (channelUID.getId()) {
case CHANNEL_SETPOINT_TEMPERATURE:
if (command instanceof QuantityType<?>) {
updateSetpointTemperature((QuantityType<?>) command);
}
break;
}
}
/**
* Updates the channels which are linked to the {@link TemperatureLevelService} of the device.
*
* @param state Current state of {@link TemperatureLevelService}.
*/
private void updateChannels(TemperatureLevelServiceState state) {
super.updateState(CHANNEL_TEMPERATURE, state.getTemperatureState());
}
/**
* Updates the channels which are linked to the {@link RoomClimateControlService} of the device.
*
* @param state Current state of {@link RoomClimateControlService}.
*/
private void updateChannels(RoomClimateControlServiceState state) {
super.updateState(CHANNEL_SETPOINT_TEMPERATURE, state.getSetpointTemperatureState());
}
/**
* Sets the desired temperature for the device.
*
* @param quantityType Command which contains the new desired temperature.
*/
private void updateSetpointTemperature(QuantityType<?> quantityType) {
QuantityType<?> celsiusType = quantityType.toUnit(SIUnits.CELSIUS);
if (celsiusType == null) {
logger.debug("Could not convert quantity command to celsius");
return;
}
double setpointTemperature = celsiusType.doubleValue();
this.updateServiceState(this.roomClimateControlService,
new RoomClimateControlServiceState(setpointTemperature));
}
}

View File

@@ -0,0 +1,133 @@
/**
* Copyright (c) 2010-2021 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.inwallswitch;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*;
import java.util.List;
import javax.measure.quantity.Energy;
import javax.measure.quantity.Power;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.devices.inwallswitch.dto.PowerMeterState;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchService;
import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState;
import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import com.google.gson.JsonElement;
/**
* Represents Bosch in-wall switches.
*
* @author Stefan Kästle - Initial contribution
*/
@NonNullByDefault
public class BoschInWallSwitchHandler extends BoschSHCHandler {
private final PowerSwitchService powerSwitchService;
public BoschInWallSwitchHandler(Thing thing) {
super(thing);
this.powerSwitchService = new PowerSwitchService();
}
@Override
protected void initializeServices() throws BoschSHCException {
super.initializeServices();
this.registerService(this.powerSwitchService, this::updateChannels, List.of(CHANNEL_POWER_SWITCH));
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
logger.debug("Handle command for: {} - {}", channelUID.getThingUID(), command);
if (command instanceof RefreshType) {
switch (channelUID.getId()) {
case CHANNEL_POWER_CONSUMPTION: {
PowerMeterState state = this.getState("PowerMeter", PowerMeterState.class);
if (state != null) {
updatePowerMeterState(state);
}
break;
}
case CHANNEL_ENERGY_CONSUMPTION:
// Nothing to do here, since the same update is received from POWER_CONSUMPTION
break;
default:
logger.warn("Received refresh request for unsupported channel: {}", channelUID);
}
} else {
switch (channelUID.getId()) {
case CHANNEL_POWER_SWITCH:
if (command instanceof OnOffType) {
updatePowerSwitchState((OnOffType) command);
}
break;
}
}
}
void updatePowerMeterState(PowerMeterState state) {
logger.debug("Parsed power meter state of {}: energy {} - power {}", this.getBoschID(), state.energyConsumption,
state.energyConsumption);
updateState(CHANNEL_POWER_CONSUMPTION, new QuantityType<Power>(state.powerConsumption, Units.WATT));
updateState(CHANNEL_ENERGY_CONSUMPTION, new QuantityType<Energy>(state.energyConsumption, Units.WATT_HOUR));
}
/**
* Updates the channels which are linked to the {@link PowerSwitchService} of the device.
*
* @param state Current state of {@link PowerSwitchService}.
*/
private void updateChannels(PowerSwitchServiceState state) {
State powerState = OnOffType.from(state.switchState.toString());
super.updateState(CHANNEL_POWER_SWITCH, powerState);
}
private void updatePowerSwitchState(OnOffType command) {
PowerSwitchServiceState state = new PowerSwitchServiceState();
state.switchState = PowerSwitchState.valueOf(command.toFullString());
this.updateServiceState(this.powerSwitchService, state);
}
@Override
public void processUpdate(String id, JsonElement state) {
super.processUpdate(id, state);
logger.debug("in-wall switch: received update: ID {} state {}", id, state);
if (id.equals("PowerMeter")) {
PowerMeterState powerMeterState = GSON.fromJson(state, PowerMeterState.class);
if (powerMeterState == null) {
logger.warn("Received unknown update in in-wall switch: {}", state);
} else {
updatePowerMeterState(powerMeterState);
}
}
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2021 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.inwallswitch.dto;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
/**
* PowerMeterState
*
* @author Stefan Kästle - Initial contribution
*/
public class PowerMeterState extends BoschSHCServiceState {
public PowerMeterState() {
super("powerMeterState");
}
public double energyConsumption;
public double powerConsumption;
}

View File

@@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2021 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.motiondetector;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_LATEST_MOTION;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.devices.motiondetector.dto.LatestMotionState;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import com.google.gson.JsonElement;
/**
* MotionDetectorHandler
*
* @author Stefan Kästle - Initial contribution
*/
@NonNullByDefault
public class MotionDetectorHandler extends BoschSHCHandler {
public MotionDetectorHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Handle command for: {} - {}", channelUID.getThingUID(), command);
if (CHANNEL_LATEST_MOTION.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
LatestMotionState state = this.getState("LatestMotion", LatestMotionState.class);
if (state != null) {
updateLatestMotionState(state);
}
}
}
}
void updateLatestMotionState(LatestMotionState state) {
DateTimeType date = new DateTimeType(state.latestMotionDetected);
updateState(CHANNEL_LATEST_MOTION, date);
}
@Override
public void processUpdate(String id, JsonElement state) {
logger.debug("Motion detector: received update: {} {}", id, state);
@Nullable
LatestMotionState latestMotionState = GSON.fromJson(state, LatestMotionState.class);
if (latestMotionState == null) {
logger.warn("Received unknown update in in-wall switch: {}", state);
return;
}
updateLatestMotionState(latestMotionState);
}
}

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2021 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.motiondetector.dto;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
/**
* {
* "result": [
* {
* "path": "/devices/hdm:ZigBee:000d6f0004b95a62/services/LatestMotion",
* "@type": "DeviceServiceData",
* "id": "LatestMotion",
* "state": {
* "latestMotionDetected": "2020-04-03T19:02:19.054Z",
* "@type": "latestMotionState"
* },
* "deviceId": "hdm:ZigBee:000d6f0004b95a62"
* }
* ],
* "jsonrpc": "2.0"
* }
*
* @author Stefan Kästle - Initial contribution
*/
public class LatestMotionState extends BoschSHCServiceState {
public LatestMotionState() {
super("latestMotionState");
}
public String latestMotionDetected;
}

View File

@@ -0,0 +1,106 @@
/**
* Copyright (c) 2010-2021 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.shuttercontrol;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_LEVEL;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.shuttercontrol.OperationState;
import org.openhab.binding.boschshc.internal.services.shuttercontrol.ShutterControlService;
import org.openhab.binding.boschshc.internal.services.shuttercontrol.dto.ShutterControlServiceState;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
/**
* Handler for a shutter control device
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public class ShutterControlHandler extends BoschSHCHandler {
/**
* Utility functions to convert data between Bosch things and openHAB items
*/
static 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;
}
}
private ShutterControlService shutterControlService;
public ShutterControlHandler(Thing thing) {
super(thing);
this.shutterControlService = new ShutterControlService();
}
@Override
protected void initializeServices() throws BoschSHCException {
super.initializeServices();
this.registerService(this.shutterControlService, this::updateChannels, List.of(CHANNEL_LEVEL));
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
if (command instanceof UpDownType) {
// Set full close/open as target state
UpDownType upDownType = (UpDownType) command;
ShutterControlServiceState state = new ShutterControlServiceState();
if (upDownType == UpDownType.UP) {
state.level = 1.0;
} else if (upDownType == UpDownType.DOWN) {
state.level = 0.0;
} else {
logger.warn("Received unknown UpDownType command: {}", upDownType);
return;
}
this.updateServiceState(this.shutterControlService, state);
} else if (command instanceof StopMoveType) {
StopMoveType stopMoveType = (StopMoveType) command;
if (stopMoveType == StopMoveType.STOP) {
// Set STOPPED operation state
ShutterControlServiceState state = new ShutterControlServiceState();
state.operationState = OperationState.STOPPED;
this.updateServiceState(this.shutterControlService, state);
}
} else if (command instanceof PercentType) {
// Set specific level
PercentType percentType = (PercentType) command;
double level = DataConversion.openPercentageToLevel(percentType.doubleValue());
this.updateServiceState(this.shutterControlService, new ShutterControlServiceState(level));
}
}
private void updateChannels(ShutterControlServiceState state) {
if (state.level != null) {
// Convert level to open ratio
int openPercentage = DataConversion.levelToOpenPercentage(state.level);
updateState(CHANNEL_LEVEL, new PercentType(openPercentage));
}
}
}

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2021 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.thermostat;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_VALVE_TAPPET_POSITION;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.temperaturelevel.TemperatureLevelService;
import org.openhab.binding.boschshc.internal.services.temperaturelevel.dto.TemperatureLevelServiceState;
import org.openhab.binding.boschshc.internal.services.valvetappet.ValveTappetService;
import org.openhab.binding.boschshc.internal.services.valvetappet.dto.ValveTappetServiceState;
import org.openhab.core.thing.Thing;
/**
* Handler for a thermostat device.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public final class ThermostatHandler extends BoschSHCHandler {
public ThermostatHandler(Thing thing) {
super(thing);
}
@Override
protected void initializeServices() throws BoschSHCException {
this.createService(TemperatureLevelService::new, this::updateChannels, List.of(CHANNEL_TEMPERATURE));
this.createService(ValveTappetService::new, this::updateChannels, List.of(CHANNEL_VALVE_TAPPET_POSITION));
}
/**
* Updates the channels which are linked to the {@link TemperatureLevelService} of the device.
*
* @param state Current state of {@link TemperatureLevelService}.
*/
private void updateChannels(TemperatureLevelServiceState state) {
super.updateState(CHANNEL_TEMPERATURE, state.getTemperatureState());
}
/**
* Updates the channels which are linked to the {@link ValveTappetService} of the device.
*
* @param state Current state of {@link ValveTappetService}.
*/
private void updateChannels(ValveTappetServiceState state) {
super.updateState(CHANNEL_VALVE_TAPPET_POSITION, state.getPositionState());
}
}

View File

@@ -0,0 +1,92 @@
/**
* Copyright (c) 2010-2021 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.twinguard;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*;
import javax.measure.quantity.Dimensionless;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.devices.twinguard.dto.AirQualityLevelState;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import com.google.gson.JsonElement;
import com.google.gson.JsonSyntaxException;
/**
* The {@link BoschSHCHandler} is responsible for handling commands for the TwinGuard handler.
*
* @author Stefan Kästle - Initial contribution
*/
@NonNullByDefault
public class BoschTwinguardHandler extends BoschSHCHandler {
public BoschTwinguardHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
Bridge bridge = this.getBridge();
if (bridge != null) {
logger.debug("Handle command for: {} - {}", channelUID.getThingUID(), command);
if (command instanceof RefreshType) {
AirQualityLevelState state = this.getState("AirQualityLevel", AirQualityLevelState.class);
if (state != null) {
updateAirQualityState(state);
}
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Bridge is NUL");
}
}
void updateAirQualityState(AirQualityLevelState state) {
updateState(CHANNEL_TEMPERATURE, new QuantityType<Temperature>(state.temperature, SIUnits.CELSIUS));
updateState(CHANNEL_TEMPERATURE_RATING, new StringType(state.temperatureRating));
updateState(CHANNEL_HUMIDITY, new QuantityType<Dimensionless>(state.humidity, Units.ONE));
updateState(CHANNEL_HUMIDITY_RATING, new StringType(state.humidityRating));
updateState(CHANNEL_PURITY, new QuantityType<Dimensionless>(state.purity, Units.ONE));
updateState(CHANNEL_AIR_DESCRIPTION, new StringType(state.description));
updateState(CHANNEL_PURITY_RATING, new StringType(state.purityRating));
updateState(CHANNEL_COMBINED_RATING, new StringType(state.combinedRating));
}
@Override
public void processUpdate(String id, JsonElement state) throws JsonSyntaxException {
logger.debug("Twinguard: received update: {} {}", id, state);
AirQualityLevelState parsed = GSON.fromJson(state, AirQualityLevelState.class);
if (parsed == null) {
logger.warn("Received unknown update in in-wall switch: {}", state);
return;
}
logger.debug("Parsed switch state of {}: {}", this.getBoschID(), parsed);
updateAirQualityState(parsed);
}
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2021 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.twinguard.dto;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
/**
* Represents the state of a device as reported from the Smart Home Controller
*
* @author Stefan Kästle - Initial contribution
*/
public class AirQualityLevelState extends BoschSHCServiceState {
public AirQualityLevelState() {
super("airQualityLevelState");
}
/*
* {"maxTemperature":25,"minTemperature":20,"custom":false,"name":"HALLWAY","maxHumidity":60,"minHumidity":40,
* "maxPurity":1000}
*/
class ComfortZone {
double maxTemperature;
double minTemperature;
boolean custom;
String name;
double maxHumidity;
double minHumidity;
double maxPurity;
}
/**
* {"temperatureRating":"GOOD","humidityRating":"MEDIUM","purity":620,"comfortZone":....,"@type":"airQualityLevelState",
* "purityRating":"GOOD","temperature":23.77,"description":"LITTLE_DRY","humidity":32.69,"combinedRating":"MEDIUM"}
*/
public String temperatureRating;
public String humidityRating;
public int purity;
public ComfortZone comfortZone;
public String purityRating;
public double temperature;
public String description;
public double humidity;
public String combinedRating;
}

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2021 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.windowcontact;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_CONTACT;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactService;
import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactState;
import org.openhab.binding.boschshc.internal.services.shuttercontact.dto.ShutterContactServiceState;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.State;
/**
* The {@link BoschSHCHandler} is responsible for handling Bosch window/door contacts.
*
* @author Stefan Kästle - Initial contribution
*/
@NonNullByDefault
public class WindowContactHandler extends BoschSHCHandler {
public WindowContactHandler(Thing thing) {
super(thing);
}
@Override
protected void initializeServices() throws BoschSHCException {
this.createService(ShutterContactService::new, this::updateChannels, List.of(CHANNEL_CONTACT));
}
private void updateChannels(ShutterContactServiceState state) {
State contact = state.value == ShutterContactState.CLOSED ? OpenClosedType.CLOSED : OpenClosedType.OPEN;
updateState(CHANNEL_CONTACT, contact);
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2021 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.exceptions;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception class for Bosch Smart Home controller errors.
*
* @author Gerd Zanker - Initial contribution
*/
@SuppressWarnings("serial")
@NonNullByDefault
public class BoschSHCException extends Exception {
public BoschSHCException() {
}
public BoschSHCException(String message) {
super(message);
}
public BoschSHCException(String message, Throwable e) {
super(message, e);
}
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2021 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.exceptions;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Thrown if the long polling failed
*
* @author Christian Oeing - Initial contribution
*/
@SuppressWarnings("serial")
@NonNullByDefault
public class LongPollingFailedException extends BoschSHCException {
public LongPollingFailedException(String message, Throwable e) {
super(message, e);
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2021 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.exceptions;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Thrown if the pairing failed multiple times
*
* @author Gerd Zanker - Initial contribution
*/
@SuppressWarnings("serial")
@NonNullByDefault
public class PairingFailedException extends BoschSHCException {
public PairingFailedException() {
}
public PairingFailedException(String message) {
super(message);
}
public PairingFailedException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,198 @@
/**
* Copyright (c) 2010-2021 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.services;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.boschshc.internal.devices.bridge.BoschSHCBridgeHandler;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
/**
* Base class of a service of a Bosch Smart Home device.
* The services of the devices and their official APIs can be found here: https://apidocs.bosch-smarthome.com/local/
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public abstract class BoschSHCService<TState extends BoschSHCServiceState> {
private final Logger logger = LoggerFactory.getLogger(BoschSHCService.class);
/**
* Unique service name
*/
private final String serviceName;
/**
* Class of service state
*/
private final Class<TState> stateClass;
/**
* gson instance to convert a class to json string and back.
*/
private final Gson gson = new Gson();
/**
* Bridge to use for communication from/to the device
*/
private @Nullable BoschSHCBridgeHandler bridgeHandler;
/**
* Id of device the service belongs to
*/
private @Nullable String deviceId;
/**
* Function to call after receiving state updates from the device
*/
private @Nullable Consumer<TState> stateUpdateListener;
/**
* Constructor
*
* @param serviceName Unique name of the service.
* @param stateClass State class that this service uses for data transfers from/to the device.
*/
protected BoschSHCService(String serviceName, Class<TState> stateClass) {
this.serviceName = serviceName;
this.stateClass = stateClass;
}
/**
* Initializes the service
*
* @param bridgeHandler Bridge to use for communication from/to the device
* @param deviceId Id of device this service is for
* @param stateUpdateListener Function to call when a state update was received from the device.
*/
public void initialize(BoschSHCBridgeHandler bridgeHandler, String deviceId,
@Nullable Consumer<TState> stateUpdateListener) {
this.bridgeHandler = bridgeHandler;
this.deviceId = deviceId;
this.stateUpdateListener = stateUpdateListener;
}
/**
* Returns the unique name of this service.
*
* @return Unique name of the service.
*/
public String getServiceName() {
return this.serviceName;
}
/**
* Returns the class of the state this service provides.
*
* @return Class of the state this service provides.
*/
public Class<TState> getStateClass() {
return this.stateClass;
}
/**
* Requests the current state of the service and updates it.
*
* @throws ExecutionException
* @throws TimeoutException
* @throws InterruptedException
* @throws BoschSHCException
*/
public void refreshState() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
@Nullable
TState state = this.getState();
if (state != null) {
this.onStateUpdate(state);
}
}
/**
* Requests the current state of the device with the specified id.
*
* @return Current state of the device.
* @throws ExecutionException
* @throws TimeoutException
* @throws InterruptedException
* @throws BoschSHCException
*/
public @Nullable TState getState()
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
String deviceId = this.deviceId;
if (deviceId == null) {
return null;
}
BoschSHCBridgeHandler bridgeHandler = this.bridgeHandler;
if (bridgeHandler == null) {
return null;
}
return bridgeHandler.getState(deviceId, this.serviceName, this.stateClass);
}
/**
* Sets the state of the device with the specified id.
*
* @param state State to set.
* @throws InterruptedException
* @throws ExecutionException
* @throws TimeoutException
*/
public void setState(TState state) throws InterruptedException, TimeoutException, ExecutionException {
String deviceId = this.deviceId;
if (deviceId == null) {
return;
}
BoschSHCBridgeHandler bridgeHandler = this.bridgeHandler;
if (bridgeHandler == null) {
return;
}
bridgeHandler.putState(deviceId, this.serviceName, state);
}
/**
* A state update was received from the bridge
*
* @param stateData Current state of service. Serialized as JSON.
*/
public void onStateUpdate(JsonElement stateData) {
@Nullable
TState state = gson.fromJson(stateData, this.stateClass);
if (state == null) {
this.logger.warn("Received invalid, expected type {}", this.stateClass.getName());
return;
}
this.onStateUpdate(state);
}
/**
* A state update was received from the bridge.
*
* @param state Current state of service as an instance of the state class.
*/
private void onStateUpdate(TState state) {
Consumer<TState> stateUpdateListener = this.stateUpdateListener;
if (stateUpdateListener != null) {
stateUpdateListener.accept(state);
}
}
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2021 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.services.dto;
import com.google.gson.annotations.SerializedName;
/**
* Base Bosch Smart Home Controller service state.
*
* @author Christian Oeing - Initial contribution
*/
public class BoschSHCServiceState {
@SerializedName("@type")
private final String type;
protected BoschSHCServiceState(String type) {
this.type = type;
}
public String getType() {
return type;
}
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2021 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.services.dto;
/**
* Generic error response of the Bosch REST API.
*
* @author Christian Oeing - Initial contribution
*/
public class JsonRestExceptionResponse extends BoschSHCServiceState {
public JsonRestExceptionResponse() {
super("JsonRestExceptionResponseEntity");
this.errorCode = "";
this.statusCode = 0;
}
/**
* The error code of the occurred Exception.
*/
public String errorCode;
/**
* The HTTP status of the error.
*/
public int statusCode;
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2021 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.services.powerswitch;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.services.BoschSHCService;
import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState;
/**
* Service to get and set the state of a power switch.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public class PowerSwitchService extends BoschSHCService<PowerSwitchServiceState> {
public PowerSwitchService() {
super("PowerSwitch", PowerSwitchServiceState.class);
}
}

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2021 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.services.powerswitch;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Possible states of a power switch.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public enum PowerSwitchState {
ON,
OFF
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2021 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.services.powerswitch.dto;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState;
/**
* Represents the state of a power switch device as reported from the Smart Home Controller
*
* @author Stefan Kästle - Initial contribution
* @author Christian Oeing - Adjustments to match general service state structure
*/
public class PowerSwitchServiceState extends BoschSHCServiceState {
public PowerSwitchServiceState() {
super("powerSwitchState");
}
/**
* Current state of power switch.
*/
public PowerSwitchState switchState;
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2021 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.services.roomclimatecontrol;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.services.BoschSHCService;
import org.openhab.binding.boschshc.internal.services.roomclimatecontrol.dto.RoomClimateControlServiceState;
/**
* Service of a virtual device which controls the radiator thermostats in a room.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public class RoomClimateControlService extends BoschSHCService<RoomClimateControlServiceState> {
public RoomClimateControlService() {
super("RoomClimateControl", RoomClimateControlServiceState.class);
}
}

View File

@@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2021 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.services.roomclimatecontrol.dto;
import javax.measure.quantity.Temperature;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.types.State;
/**
* State for {@link RoomClimateControlService} to get and set the desired temperature of a room.
*
* @author Christian Oeing - Initial contribution
*/
public class RoomClimateControlServiceState extends BoschSHCServiceState {
private static final String TYPE = "climateControlState";
public RoomClimateControlServiceState() {
super(TYPE);
}
/**
* Constructor.
*
* @param setpointTemperature Desired temperature (in degree celsius).
*/
public RoomClimateControlServiceState(double setpointTemperature) {
super(TYPE);
this.setpointTemperature = setpointTemperature;
}
/**
* Desired temperature (in degree celsius).
*
* @apiNote Min: 5.0, Max: 30.0.
* @apiNote Can be set in 0.5 steps.
*/
private double setpointTemperature;
/**
* Desired temperature state to set for a thing.
*
* @return Desired temperature state to set for a thing.
*/
public State getSetpointTemperatureState() {
return new QuantityType<Temperature>(this.setpointTemperature, SIUnits.CELSIUS);
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2021 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.services.shuttercontact;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.services.BoschSHCService;
import org.openhab.binding.boschshc.internal.services.shuttercontact.dto.ShutterContactServiceState;
/**
* Service to get the state of shutters.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public class ShutterContactService extends BoschSHCService<ShutterContactServiceState> {
public ShutterContactService() {
super("ShutterContact", ShutterContactServiceState.class);
}
}

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2021 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.services.shuttercontact;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Possible values for shutter contacts.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public enum ShutterContactState {
OPEN,
CLOSED;
}

View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2021 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.services.shuttercontact.dto;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactState;
/**
* State for the shutter contact service
*
* @author Christian Oeing - Initial contribution
*/
public class ShutterContactServiceState extends BoschSHCServiceState {
/**
* Current state of shutter contact.
*/
public ShutterContactState value;
public ShutterContactServiceState() {
super("shutterContactState");
}
}

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2021 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.services.shuttercontrol;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Operation State.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public enum OperationState {
MOVING,
STOPPED;
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2021 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.services.shuttercontrol;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.services.BoschSHCService;
import org.openhab.binding.boschshc.internal.services.shuttercontrol.dto.ShutterControlServiceState;
/**
* Service to control the shutters of a device.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public class ShutterControlService extends BoschSHCService<ShutterControlServiceState> {
public ShutterControlService() {
super("ShutterControl", ShutterControlServiceState.class);
}
}

View File

@@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2021 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.services.shuttercontrol.dto;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
import org.openhab.binding.boschshc.internal.services.shuttercontrol.OperationState;
/**
* State for a shutter control device
*
* @author Christian Oeing - Initial contribution
*/
public class ShutterControlServiceState extends BoschSHCServiceState {
/**
* Current open ratio of shutter (0.0 [closed] to 1.0 [open])
*/
public Double level;
/**
* Current operation state of shutter
*/
public OperationState operationState;
public ShutterControlServiceState() {
super("shutterControlState");
this.operationState = OperationState.STOPPED;
}
public ShutterControlServiceState(double level) {
this();
this.level = level;
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2021 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.services.temperaturelevel;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.services.BoschSHCService;
import org.openhab.binding.boschshc.internal.services.temperaturelevel.dto.TemperatureLevelServiceState;
/**
* TemperatureLevel service.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public class TemperatureLevelService extends BoschSHCService<TemperatureLevelServiceState> {
public TemperatureLevelService() {
super("TemperatureLevel", TemperatureLevelServiceState.class);
}
}

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2021 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.services.temperaturelevel.dto;
import javax.measure.quantity.Temperature;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.types.State;
/**
* TemperatureLevel service state.
*
* @author Christian Oeing - Initial contribution
*/
public class TemperatureLevelServiceState extends BoschSHCServiceState {
public TemperatureLevelServiceState() {
super("temperatureLevelState");
}
/**
* Current temperature (in degree celsius)
*/
private double temperature;
/**
* Current temperature state to set for a thing.
*
* @return Current temperature state to use for a thing.
*/
public State getTemperatureState() {
return new QuantityType<Temperature>(this.temperature, SIUnits.CELSIUS);
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2021 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.services.valvetappet;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.services.BoschSHCService;
import org.openhab.binding.boschshc.internal.services.valvetappet.dto.ValveTappetServiceState;
/**
* Valve Tappet service.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public class ValveTappetService extends BoschSHCService<ValveTappetServiceState> {
public ValveTappetService() {
super("ValveTappet", ValveTappetServiceState.class);
}
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2021 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.services.valvetappet.dto;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.types.State;
/**
* Valve Tappet service state.
*
* @author Christian Oeing - Initial contribution
*/
public class ValveTappetServiceState extends BoschSHCServiceState {
public ValveTappetServiceState() {
super("valveTappetState");
}
/**
* Current open percentage of valve tappet (0 [closed] - 100 [open]).
*/
private int position;
/**
* Current position state of valve tappet.
*/
public State getPositionState() {
return new DecimalType(this.position);
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="boschshc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Bosch Smart Home Binding</name>
<description>This is the binding for Bosch Smart Home Controller.</description>
<author>Stefan Kästle</author>
</binding:binding>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:boschshc:bridge">
<parameter name="ipAddress" type="text" required="true">
<context>network-address</context>
<label>Network Address</label>
<description>Network address of the Bosch Smart Home Controller.</description>
</parameter>
<parameter name="password" type="text" required="true">
<label>System Password</label>
<context>password</context>
<description>The system password of the Bosch Smart Home Controller necessary for pairing.</description>
</parameter>
</config-description>
<config-description uri="thing-type:boschshc:device">
<parameter name="id" type="text" required="true">
<label>Device ID</label>
<description>Unique ID of the device.</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,5 @@
# Thing status offline descriptions
offline.conf-error-pairing = Press pairing button on the Bosch Smart Home Controller.
offline.not-reachable = Smart Home Controller is not reachable.
offline.conf-error-ssl = The SSL connection to the Bosch Smart Home Controller is not possible.

View File

@@ -0,0 +1,9 @@
# binding
binding.boschshc.name = Bosch Smart Home Controller Binding
binding.boschshc.description = Dieses Binding integriert das Bosch Smart Home System. Durch diese können die Bosch Smart Home Geräte verwendet werden.
# Thing status offline descriptions
offline.conf-error-pairing = Bitte betätigen Sie den Taster am Bosch Smart Home Controller zum automatischen Verbinden.
offline.not-reachable = Smart Home Controller ist nicht erreichbar.
offline.conf-error-ssl = Die SSL Verbindung zum Bosch Smart Home Controller ist nicht möglich.

View File

@@ -0,0 +1,262 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="boschshc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Bosch Bridge -->
<bridge-type id="shc">
<label>Smart Home Controller</label>
<description>The Bosch SHC Bridge representing the Bosch Smart Home Controller.</description>
<config-description-ref uri="thing-type:boschshc:bridge"/>
</bridge-type>
<thing-type id="in-wall-switch">
<supported-bridge-type-refs>
<bridge-type-ref id="shc"/>
</supported-bridge-type-refs>
<label>In-wall Switch</label>
<description>Bosch In-wall switch for light control</description>
<channels>
<channel id="power-switch" typeId="system.power"/>
<channel id="power-consumption" typeId="power-consumption"/>
<channel id="energy-consumption" typeId="energy-consumption"/>
</channels>
<config-description-ref uri="thing-type:boschshc:device"/>
</thing-type>
<thing-type id="twinguard">
<supported-bridge-type-refs>
<bridge-type-ref id="shc"/>
</supported-bridge-type-refs>
<label>TwinGuard</label>
<description>Bosch TwinGuard environmental sensor</description>
<channels>
<channel id="temperature" typeId="temperature"/>
<channel id="temperature-rating" typeId="temperature-rating"/>
<channel id="humidity" typeId="humidity"/>
<channel id="humidity-rating" typeId="humidity-rating"/>
<channel id="purity" typeId="purity"/>
<channel id="air-description" typeId="air-description"/>
<channel id="purity-rating" typeId="purity-rating"/>
<channel id="combined-rating" typeId="combined-rating"/>
</channels>
<config-description-ref uri="thing-type:boschshc:device"/>
</thing-type>
<thing-type id="window-contact">
<supported-bridge-type-refs>
<bridge-type-ref id="shc"/>
</supported-bridge-type-refs>
<label>Window/Door Contact</label>
<description>Bosch Contact for windows and doors</description>
<channels>
<channel id="contact" typeId="contact"/>
</channels>
<config-description-ref uri="thing-type:boschshc:device"/>
</thing-type>
<thing-type id="motion-detector">
<supported-bridge-type-refs>
<bridge-type-ref id="shc"/>
</supported-bridge-type-refs>
<label>Motion Detector</label>
<description>Bosch Motion Detector</description>
<channels>
<channel id="latest-motion" typeId="latest-motion"/>
</channels>
<config-description-ref uri="thing-type:boschshc:device"/>
</thing-type>
<thing-type id="shutter-control">
<supported-bridge-type-refs>
<bridge-type-ref id="shc"/>
</supported-bridge-type-refs>
<label>Shutter Control</label>
<description>Bosch Shutter Control</description>
<channels>
<channel id="level" typeId="level"/>
</channels>
<config-description-ref uri="thing-type:boschshc:device"/>
</thing-type>
<thing-type id="thermostat">
<supported-bridge-type-refs>
<bridge-type-ref id="shc"/>
</supported-bridge-type-refs>
<label>Thermostat</label>
<description>Bosch Thermostat</description>
<channels>
<channel id="temperature" typeId="temperature"/>
<channel id="valve-tappet-position" typeId="valve-tappet-position"/>
</channels>
<config-description-ref uri="thing-type:boschshc:device"/>
</thing-type>
<thing-type id="climate-control">
<supported-bridge-type-refs>
<bridge-type-ref id="shc"/>
</supported-bridge-type-refs>
<label>Climate Control</label>
<description>Bosch Climate Control. This is a virtual device which is automatically created for all rooms that have
thermostats in it.</description>
<channels>
<channel id="temperature" typeId="temperature"/>
<channel id="setpoint-temperature" typeId="setpoint-temperature"/>
</channels>
<config-description-ref uri="thing-type:boschshc:device"/>
</thing-type>
<channel-type id="temperature">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<description>Current measured temperature.</description>
<state min="0" max="40" step="0.5" pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="temperature-rating">
<item-type>String</item-type>
<label>Temperature Rating</label>
<description>Rating of the currently measured temperature.</description>
<state readOnly="true">
<options>
<option value="GOOD">Good Temperature</option>
<option value="MEDIUM">Medium Temperature</option>
<option value="BAD">Bad Temperature</option>
</options>
</state>
</channel-type>
<channel-type id="humidity">
<item-type>Number:Dimensionless</item-type>
<label>Humidity</label>
<description>Current measured humidity.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="humidity-rating">
<item-type>String</item-type>
<label>Humidity Rating</label>
<description>Rating of current measured humidity.</description>
<state readOnly="true">
<options>
<option value="GOOD">Good Humidity</option>
<option value="MEDIUM">Medium Humidity</option>
<option value="BAD">Bad Humidity</option>
</options>
</state>
</channel-type>
<channel-type id="energy-consumption">
<item-type>Number:Energy</item-type>
<label>Energy consumption (Wh)</label>
<description>Energy consumption of the device.</description>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="power-consumption">
<item-type>Number:Power</item-type>
<label>Power consumption (W)</label>
<description>Current power consumption of the device.</description>
<state pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="purity">
<item-type>Number:Dimensionless</item-type>
<label>Purity</label>
<description>Purity of the air. A higher value indicates a higher pollution.</description>
<state min="500" max="5500" pattern="%.1f ppm" readOnly="true"/>
</channel-type>
<channel-type id="air-description">
<item-type>String</item-type>
<label>Description</label>
<description>Overall description of the air quality.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="purity-rating">
<item-type>String</item-type>
<label>Purity Rating</label>
<description>Rating of the air purity.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="combined-rating">
<item-type>String</item-type>
<label>Combined Rating</label>
<description>Combined rating of the air quality.</description>
<state readOnly="true">
<options>
<option value="GOOD">Good Quality</option>
<option value="MEDIUM">Medium Quality</option>
<option value="BAD">Bad Quality</option>
</options>
</state>
</channel-type>
<channel-type id="contact">
<item-type>Contact</item-type>
<label>Window/Door contact</label>
<description>A window and door contact.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="latest-motion">
<item-type>DateTime</item-type>
<label>Latest motion</label>
<description>Timestamp of the latest motion.</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="level">
<item-type>Rollershutter</item-type>
<label>Level</label>
<description>Current open ratio (0 to 100).</description>
<state min="0" max="100" step="0.5" readOnly="false"/>
</channel-type>
<channel-type id="valve-tappet-position">
<item-type>Number:Dimensionless</item-type>
<label>Valve Tappet Position</label>
<description>Current open ratio (0 to 100).</description>
<state min="0" max="100" step="1" readOnly="true"/>
</channel-type>
<channel-type id="setpoint-temperature">
<item-type>Number:Temperature</item-type>
<label>Setpoint Temperature</label>
<description>Desired temperature.</description>
<state min="5" max="30" step="0.5" pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,30 @@
-----BEGIN CERTIFICATE-----
MIIFETCCAvmgAwIBAgIUR8y7kFBqVMZCYZdSQWVuVJgSAqYwDQYJKoZIhvcNAQEL
BQAwYzELMAkGA1UEBhMCREUxITAfBgNVBAoMGEJvc2NoIFRoZXJtb3RlY2huaWsg
R21iSDExMC8GA1UEAwwoU21hcnQgSG9tZSBDb250cm9sbGVyIFByb2R1Y3RpdmUg
Um9vdCBDQTAeFw0xNTA4MTgwNzI0MjFaFw0yNTA4MTYwNzI0MjFaMFsxCzAJBgNV
BAYTAkRFMSEwHwYDVQQKDBhCb3NjaCBUaGVybW90ZWNobmlrIEdtYkgxKTAnBgNV
BAMMIFNtYXJ0IEhvbWUgQ29udHJvbGxlciBJc3N1aW5nIENBMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsBNK3PPd/E9jbf3YkZIDtfIl2Vo0Nx7oeOsh
F0L9tZwqC3+85ymB5LgFBOoHpr7tTFRb4elyPsfyv/GfXuJmDIxVAWBn/pxFzODa
J3DGJ2kvwipvMNp7IxXHhK10YsG8AaT0QaeaYGq1GRp5uNZafwAOOkrrQfwtG+za
Qn9qUxLYBrB++RN/5mk4Z7gyrq7fi84T23yMOtVkdb+mlb9qStQ3mllglqrRlJQo
MKdQxe24Farg6N3y7h5bxLJEEXGqGExDNwR46ep+4Ys7W2QeD/2LXwYvKQ+wO70+
BNxnikkq8kPcq8694HMsfzUTBrxuHQGi6td9o+3CW01AOEvV0wIDAQABo4HEMIHB
MBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFHy1ci5zZEQaHLDAaYFYez8R
FHsXMB8GA1UdIwQYMBaAFOFQaxE4w2eoyE+f6oXGTxH1V1Y+MA4GA1UdDwEB/wQE
AwIBBjBbBgNVHR8EVDBSMFCgTqBMhkpodHRwczovLzI5Lm1jZy5lc2NyeXB0LmNv
bS9jcmw/aWQ9ZTE1MDZiMTEzOGMzNjdhOGM4NGY5ZmVhODVjNjRmMTFmNTU3NTYz
ZTANBgkqhkiG9w0BAQsFAAOCAgEAZpp9kE7Qy6tcQrfW4DJAqEcUhzg4zncJYxpb
dTn/o5TvH/uPVOfoxJgtsTFtsY/ytcPJReLcgmqrRN1gTNefdXylJr688hFyhf1Z
xGDoZG8MuzM9QXaHC6UNFzaeZj46ZYfdJiUtDXsYN82opGE6GhBju5JOLoFd2vYK
qUnVKWqdrN0KkihClry6NcfiLEA70m00pNtsVZyVGyk7DP4ErVF5K3j40T5v4ZJl
Q9ri/V97zyqXeIti8kZdla7kzJBFbGEumlUyVPRpoxdpnvWM7AgTOXXsh2sCFAA1
0hUHVOwBZCylaNUXjKMtnA938ykhNCx+OCd2NpZBf3qB6+w2MS7dQuRvMsDJcnLq
X80QHJzXpmDsXEiwKyvmZnZbiAgoOiUSe2O6OaGsDRW8UBzi+wm42pxgbDnAcGUu
r9Cf5y0+SFS0aQkqcWbJYwPy+LQi2MJGkv34FxTOCqygluzZt+w5xZyq5PcpPNm5
1s4Ps2+psvNhcAG3EHRF9vBnlr1MCVU04XYig54HeNGFIQQAFWFFR/9DgnH/cFLf
gPoJEZV/VZtsOjy/EsqYZMFJBzJEtKOiTCKDe+pVirDB9zrcVsJG8LGiLd7266e9
1Eg5GjNiavG7ninMOWSJLfW4xPD6S3zxDAYjsPDJbMFqEFIF2ZvyYC1mVeflB/WM
xnZ+67w=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,33 @@
-----BEGIN CERTIFICATE-----
MIIFujCCA6KgAwIBAgIUIbQ+BIVcGVD29UIe+Sv6/+Qy/OUwDQYJKoZIhvcNAQEL
BQAwYzELMAkGA1UEBhMCREUxITAfBgNVBAoMGEJvc2NoIFRoZXJtb3RlY2huaWsg
R21iSDExMC8GA1UEAwwoU21hcnQgSG9tZSBDb250cm9sbGVyIFByb2R1Y3RpdmUg
Um9vdCBDQTAeFw0xNTA4MTgwNzIwMTNaFw0zNTA4MTQwNzIwMTNaMGMxCzAJBgNV
BAYTAkRFMSEwHwYDVQQKDBhCb3NjaCBUaGVybW90ZWNobmlrIEdtYkgxMTAvBgNV
BAMMKFNtYXJ0IEhvbWUgQ29udHJvbGxlciBQcm9kdWN0aXZlIFJvb3QgQ0EwggIi
MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCcFmt1vu85lfXMl66Ix32tmEbc
n4bt6Oa6QIiT6zJIR2DsE85c42H8XogATWiqfp3FTbmfIIijfoj9JL6uyFkw0yrT
qfttw9KD8DRIV973F1UyAP8wPxpdt2QPJCBMmqymC6h2oT7eS6hRIMbY3SFLa5lO
4EQ10uflZnY9Yv7kTzeuEw1qWqd8kHhfDBq3k2N90oopt47ghDQ/qUmne19xp0jQ
fXFA6hfudNcU9vuZ6hvObm25++ySmRKvtuY+O/CmLVnUJngpKQWJCnYOv3/Z5StZ
5aVvLR028ozc1oqdL8fVeaJX8xIdBsSjB+gOaauEYodJzVfeLdXVb8R4CqVighci
EUuwZVhzdtA5qs2O9jLJv6JFiD+uuRn8Ip1uYiajYqkRzR2egKWFfhZvV6Yk2zuw
s8FUtagtYRwKCp+F+f+PCryLcBcnyc7iVm0Xo7kQAjzoDql4vmXQybmP6kU9qzmD
xEG02s6FHVn1X1X4htXc/+Wh0/0850T+Up2HeN+ZN92BubI8yM62mecvfx08vSb1
5AviYkQQE37KzGeKYYbciEMeVu5sLx/lN6YIcyHY5kTUsU7SCzw7vTTsNjTzuzYa
l2fudHS8lOHaAwvZP//14cM+N9beQqLzxS7jdmFQxtToyzdbgL1OekO58fiqti4W
d88bnmMBZsl3bR9b5QIDAQABo2YwZDASBgNVHRMBAf8ECDAGAQH/AgECMB0GA1Ud
DgQWBBThUGsROMNnqMhPn+qFxk8R9VdWPjAfBgNVHSMEGDAWgBThUGsROMNnqMhP
n+qFxk8R9VdWPjAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAEp2
bQei/KQGrnsnqugeseDVKNTOFp5o0xYz8gXEWzRCuAIo/sYKcFWziquajJWuCt/9
CexNFWkYtV95EbunyE+wijQcnOehGSZ2gWnZiQU2fu1Y4aA5g3LlB61ljnbhX4SE
tLs31iTdjPFcWMx+rsS3+qfuOiOqQbliTykG+p/ULVLLPDCmzL/MHg3w5AiGB8k5
i1npzDKJKpLFGFWEnECYKhPi93rLfdgmOEFalIoFB96/upm6bfOWbNvsdIspFVGe
3zSjWUvveHe9mm+VTq9aldwy/J0/81oFF7C5CmlB31sDwfY+qF5/mHKfPbrnWTIi
QAiZJxXrbmeWX9JVutRbokP1UTX63ghH+BNab/E1D020JVkimMf2Vg1/5WR2gdkN
S4j+f//uVKuCr7bPGWzcADeURlyCmW/O2CNfln+T/0YFg2lET9PAEDkZ7Js3I/4f
+Dy58LwjdQYI3Z6qKA9h0Cfgy6KOA8Omyw3QmdTAAd0EgABQ/vxNVL3Q4Oh8Eiff
ZVrpFWLgMxeRckHTMqG9SfGBdZQCO7XPz7mb/8Da6prEfw4VKvdh9llvatWeB1V1
vqixwFVuHIWKxIiR8GXZEjIQXBmeuzdgIceYcw12HYHLUifFozaNtjxMcPcIALKz
GrR4oS2tFVZCjwF4vPAt15fsbEx/F/NfaO6SAFz8
-----END CERTIFICATE-----

View File

@@ -0,0 +1,103 @@
/**
* Copyright (c) 2010-2021 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 static org.junit.jupiter.api.Assertions.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult;
import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException;
/**
* Tests cases for {@link BoschHttpClient}.
*
* @author Gerd Zanker - Initial contribution
*/
@NonNullByDefault
class BoschHttpClientTest {
@Nullable
private BoschHttpClient httpClient;
@BeforeAll
static void beforeAll() {
BoschSslUtilTest.prepareTempFolderForKeyStore();
}
@BeforeEach
void beforeEach() throws PairingFailedException {
SslContextFactory sslFactory = new BoschSslUtil("127.0.0.1").getSslContextFactory();
httpClient = new BoschHttpClient("127.0.0.1", "dummy", sslFactory);
assertNotNull(httpClient);
}
@Test
void getPairingUrl() {
assertEquals("https://127.0.0.1:8443/smarthome/clients", httpClient.getPairingUrl());
}
@Test
void getBoschShcUrl() {
assertEquals("https://127.0.0.1:8444/testEndpoint", httpClient.getBoschShcUrl("testEndpoint"));
}
@Test
void getBoschSmartHomeUrl() {
assertEquals("https://127.0.0.1:8444/smarthome/endpointForTest",
httpClient.getBoschSmartHomeUrl("endpointForTest"));
}
@Test
void getServiceUrl() {
assertEquals("https://127.0.0.1:8444/smarthome/devices/testDevice/services/testService/state",
httpClient.getServiceUrl("testService", "testDevice"));
}
@Test
void isAccessPossible() throws InterruptedException {
assertFalse(httpClient.isAccessPossible());
}
@Test
void doPairing() throws InterruptedException {
assertFalse(httpClient.doPairing());
}
@Test
void createRequest() {
Request request = httpClient.createRequest("https://127.0.0.1", HttpMethod.GET);
assertNotNull(request);
}
@Test
void createRequestWithObject() {
Request request = httpClient.createRequest("https://127.0.0.1", HttpMethod.GET, "someData");
assertNotNull(request);
}
@Test
void sendRequest() {
Request request = httpClient.createRequest("https://127.0.0.1", HttpMethod.GET);
// Null pointer exception is expected, because localhost will not answer request
assertThrows(NullPointerException.class, () -> {
httpClient.sendRequest(request, SubscribeResult.class);
});
}
}

View File

@@ -0,0 +1,102 @@
/**
* Copyright (c) 2010-2021 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 static org.junit.jupiter.api.Assertions.*;
import java.io.File;
import java.nio.file.Paths;
import java.security.KeyStore;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException;
/**
* Tests cases for {@link BoschSslUtil}.
*
* @author Gerd Zanker - Initial contribution
*/
@NonNullByDefault
class BoschSslUtilTest {
@BeforeAll
static void beforeAll() {
prepareTempFolderForKeyStore();
}
public static void prepareTempFolderForKeyStore() {
// Use temp folder for userdata folder
String tmpDir = System.getProperty("java.io.tmpdir");
tmpDir = tmpDir != null ? tmpDir : "/tmp";
System.setProperty("openhab.userdata", tmpDir);
// prepare temp folder on local drive
File tempDir = Paths.get(tmpDir, "etc").toFile();
if (!tempDir.exists()) {
assertTrue(tempDir.mkdirs());
}
}
@Test
void getBoschShcClientId() {
// OpenSource Bosch SHC clients needs start with oss
assertTrue(BoschSslUtil.getBoschShcClientId().startsWith("oss"));
}
@Test
void getBoschShcServerId() {
// OpenSource Bosch SHC clients needs start with oss
assertTrue(BoschSslUtil.getBoschShcServerId("localhost").startsWith("oss"));
assertTrue(BoschSslUtil.getBoschShcServerId("localhost").contains("localhost"));
}
@Test
void getKeystorePath() {
BoschSslUtil sslUtil = new BoschSslUtil("123.45.67.89");
assertTrue(sslUtil.getKeystorePath().endsWith(".jks"));
}
/**
* Test if the keyStore can be created if it doesn't exist.
*/
@Test
void keyStoreAndFactory() throws PairingFailedException {
BoschSslUtil sslUtil1 = new BoschSslUtil("127.0.0.1");
// remote old, existing jks
File keyStoreFile = new File(sslUtil1.getKeystorePath());
keyStoreFile.deleteOnExit();
if (keyStoreFile.exists()) {
assertTrue(keyStoreFile.delete());
}
assertFalse(keyStoreFile.exists());
BoschSslUtil sslUtil2 = new BoschSslUtil("127.0.0.1");
// fist call where keystore is created
KeyStore keyStore = sslUtil2.getKeyStoreAndCreateIfNecessary();
assertNotNull(keyStore);
assertTrue(keyStoreFile.exists());
// second call where keystore is reopened
KeyStore keyStore2 = sslUtil2.getKeyStoreAndCreateIfNecessary();
assertNotNull(keyStore2);
// basic test if a SSL factory instance can be created
SslContextFactory factory = sslUtil2.getSslContextFactory();
assertNotNull(factory);
}
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2021 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 static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import com.google.gson.Gson;
/**
* Unit tests for LongPollResult
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public class LongPollResultTest {
private final Gson gson = new Gson();
@Test
public void noResultsForErrorResult() {
LongPollResult longPollResult = gson.fromJson(
"{\"jsonrpc\":\"2.0\", \"error\": { \"code\":-32001, \"message\":\"No subscription with id: e8fei62b0-0\" } }",
LongPollResult.class);
assertNotEquals(null, longPollResult);
if (longPollResult != null) {
assertEquals(null, longPollResult.result);
}
}
}