diff --git a/CODEOWNERS b/CODEOWNERS
index 363278450..870d7f28d 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -34,6 +34,7 @@
/bundles/org.openhab.binding.bluetooth.roaming/ @cpmeister
/bundles/org.openhab.binding.bluetooth.ruuvitag/ @ssalonen
/bundles/org.openhab.binding.boschindego/ @jofleck
+/bundles/org.openhab.binding.boschshc/ @stefan-kaestle @coeing @GerdZanker
/bundles/org.openhab.binding.bosesoundtouch/ @marvkis @tratho
/bundles/org.openhab.binding.bsblan/ @hypetsch
/bundles/org.openhab.binding.bticinosmarther/ @MrRonfo
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index b3039866c..c15457a38 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -156,6 +156,11 @@
org.openhab.binding.boschindego
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.boschshc
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.bosesoundtouch
diff --git a/bundles/org.openhab.binding.boschshc/DEVELOPERS.md b/bundles/org.openhab.binding.boschshc/DEVELOPERS.md
new file mode 100644
index 000000000..9ee8b4bb7
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/DEVELOPERS.md
@@ -0,0 +1,52 @@
+# For Developers
+
+## Build
+
+To only build the Bosch SHC binding code execute
+
+ mvn -pl :org.openhab.binding.boschshc install
+
+## Execute
+
+After compiling a new ``org.openhab.binding.boschshc.jar``
+copy it into the ``addons`` folder of your openHAB test instance.
+
+For the first time the jar is loaded automatically as a bundle.
+
+It should also be reloaded automatically when the jar changed.
+
+To reload the bundle manually you need to execute:
+
+ bundle:update "openHAB Add-ons :: Bundles :: BoschSHC Binding"
+
+or get the ID and update the bundle using the ID:
+
+ bundle:list
+ -> Get ID for "openHAB Add-ons :: Bundles :: BoschSHC Binding"
+ bundle:update
+
+
+## Debugging
+
+To get debug output and traces of the Bosch SHC binding code
+add the following lines into ``userdata/etc/log4j2.xml`` Loggers XML section.
+
+
+
+
+## Pairing and Certificates
+
+We need secured and paired connection from the openHAB binding instance to the Bosch SHC.
+
+Read more about the pairing process in [register a new client to the bosch smart home controller](https://github.com/BoschSmartHome/bosch-shc-api-docs/tree/master/postman#register-a-new-client-to-the-bosch-smart-home-controller)
+
+A precondition for the secured connection to the Bosch SHC is a self singed key + certificate.
+The key + certificate will be created and stored with the public Bosch SHC certificates in a Java Key store (jks).
+
+The public certificates files are from https://github.com/BoschSmartHome/bosch-shc-api-docs/tree/master/best_practice.
+File copies stored in ``src/main/resource``.
+
+All three certificates and the key will be used for the HTTPS connection between
+this openHAB binding and the Bosch SHC.
+
+During pairing the openHAB binding will exchange the self singed certificate with SHC.
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.boschshc/NOTICE b/bundles/org.openhab.binding.boschshc/NOTICE
new file mode 100644
index 000000000..bc35a2055
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/NOTICE
@@ -0,0 +1,21 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
+
+## Third-party Content
+
+bcpkix-jdk15on
+bcprov-jdk15on
+* License: Bouncy Castle License
+* Project: https://www.bouncycastle.org
+* Source: https://github.com/bcgit/bc-java
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.boschshc/README.md b/bundles/org.openhab.binding.boschshc/README.md
new file mode 100644
index 000000000..b55abc659
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/README.md
@@ -0,0 +1,170 @@
+# BoschSHC Binding
+
+Binding for the Bosch Smart Home Controller.
+
+- [BoschSHC Binding](#boschshc-binding)
+ - [Supported Things](#supported-things)
+ - [Bosch In-Wall switches & Bosch Smart Plugs](#bosch-in-wall-switches--bosch-smart-plugs)
+ - [Bosch TwinGuard smoke detector](#bosch-twinguard-smoke-detector)
+ - [Bosch Window/Door contacts](#bosch-windowdoor-contacts)
+ - [Bosch Motion Detector](#bosch-motion-detector)
+ - [Bosch Shutter Control in-wall](#bosch-shutter-control-in-wall)
+ - [Bosch Thermostat](#bosch-thermostat)
+ - [Bosch Climate Control](#bosch-climate-control)
+ - [Limitations](#limitations)
+ - [Discovery](#discovery)
+ - [Binding Configuration](#binding-configuration)
+ - [Getting the device IDs](#getting-the-device-ids)
+ - [Thing Configuration](#thing-configuration)
+ - [Item Configuration](#item-configuration)
+
+## Supported Things
+
+### Bosch In-Wall switches & Bosch Smart Plugs
+
+**Thing Type ID**: `in-wall-switch`
+
+| Channel Type ID | Item Type | Description |
+|--------------------|---------------|----------------------------------------------|
+| power-switch | Switch | Current state of the switch. |
+| power-consumption | Number:Power | Current power consumption (W) of the device. |
+| energy-consumption | Number:Energy | Energy consumption of the device. |
+
+### Bosch TwinGuard smoke detector
+
+**Thing Type ID**: `twinguard`
+
+| Channel Type ID | Item Type | Description |
+|--------------------|----------------------|---------------------------------------------------------------------------------------------------|
+| temperature | Number:Temperature | Current measured temperature. |
+| temperature-rating | String | Rating of the currently measured temperature. |
+| humidity | Number:Dimensionless | Current measured humidity. |
+| humidity-rating | String | Rating of current measured humidity. |
+| purity | Number:Dimensionless | Purity of the air (ppm). Range from 500 to 5500 ppm. A higher value indicates a higher pollution. |
+| purity-rating | String | Rating of current measured purity. |
+| air-description | String | Overall description of the air quality. |
+| combined-rating | String | Combined rating of the air quality. |
+
+### Bosch Window/Door contacts
+
+**Thing Type ID**: `window-contact`
+
+| Channel Type ID | Item Type | Description |
+|-----------------|-----------|------------------------------|
+| contact | Contact | Contact state of the device. |
+
+### Bosch Motion Detector
+
+**Thing Type ID**: `motion-detector`
+
+| Channel Type ID | Item Type | Description |
+|-----------------|-----------|--------------------------------|
+| latest-motion | DateTime | The date of the latest motion. |
+
+### Bosch Shutter Control in-wall
+
+**Thing Type ID**: `shutter-control`
+
+| Channel Type ID | Item Type | Description |
+|-----------------|---------------|------------------------------------------|
+| level | Rollershutter | Current open ratio (0 to 100, Step 0.5). |
+
+### Bosch Thermostat
+
+**Thing Type ID**: `thermostat`
+
+| Channel Type ID | Item Type | Description |
+|-----------------------|----------------------|------------------------------------------------|
+| temperature | Number:Temperature | Current measured temperature. |
+| valve-tappet-position | Number:Dimensionless | Current open ratio of valve tappet (0 to 100). |
+
+### Bosch Climate Control
+
+**Thing Type ID**: `climate-control`
+
+| Channel Type ID | Item Type | Description |
+|----------------------|--------------------|-------------------------------|
+| temperature | Number:Temperature | Current measured temperature. |
+| setpoint-temperature | Number:Temperature | Desired temperature. |
+
+## Limitations
+
+- Discovery of Things
+- Discovery of Bridge
+
+## Discovery
+
+Configuration via configuration files or UI (see below).
+
+## Bridge Configuration
+
+You need to provide the IP address and the system password of your Bosch Smart Home Controller.
+The IP address of the controller is visible in the Bosch Smart Home Mobile App (More -> System -> Smart Home Controller) or in your network router UI.
+The system password is set by you during your initial registration steps in the _Bosch Smart Home App_.
+
+A keystore file with a self signed certificate is created automatically.
+This certificate is used for pairing between the Bridge and the Bosch SHC.
+
+*Press and hold the Bosch Smart Home Controller Bridge button until the LED starts blinking after you save your settings for pairing*.
+
+## Getting the device IDs
+
+Bosch IDs for found devices are displayed in the openHAB log on bootup (`OPENHAB_FOLDER/userdata/logs/openhab.log`)
+
+Example:
+
+```
+2020-08-11 12:42:49.490 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Heizung id=hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX
+2020-08-11 12:42:49.495 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=-RoomClimateControl- id=roomClimateControl_hz_1
+2020-08-11 12:42:49.497 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=-VentilationService- id=ventilationService
+2020-08-11 12:42:49.498 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Großes Fenster id=hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX
+2020-08-11 12:42:49.501 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=-IntrusionDetectionSystem- id=intrusionDetectionSystem
+2020-08-11 12:42:49.502 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Rollladen id=hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX
+2020-08-11 12:42:49.502 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Heizung id=hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX
+2020-08-11 12:42:49.503 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Heizung Haus id=hdm:ICom:819410185:HC1
+2020-08-11 12:42:49.503 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=-RoomClimateControl- id=roomClimateControl_hz_6
+2020-08-11 12:42:49.504 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=PhilipsHueBridgeManager id=hdm:PhilipsHueBridge:PhilipsHueBridgeManager
+2020-08-11 12:42:49.505 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Rollladen id=hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX
+2020-08-11 12:42:49.506 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Rollladen id=hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX
+2020-08-11 12:42:49.507 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Central Heating id=hdm:ICom:819410185
+```
+
+## Thing Configuration
+
+You define your Bosch devices by adding them either to a `.things` file in your `$OPENHAB_CONF/things` folder like this:
+
+```
+Bridge boschshc:shc:1 [ ipAddress="192.168.x.y", password="XXXXXXXXXX" ] {
+ Thing in-wall-switch bathroom "Bathroom" [ id="hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX" ]
+ Thing in-wall-switch bedroom "Bedroom" [ id="hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX" ]
+ Thing in-wall-switch kitchen "Kitchen" [ id="hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX" ]
+ Thing in-wall-switch corridor "Corridor" [ id="hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX" ]
+ Thing in-wall-switch livingroom "Living Room" [ id="hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX" ]
+
+ Thing in-wall-switch coffeemachine "Coffee Machine" [ id="hdm:HomeMaticIP:3014F711A0000XXXXXXXXXXXX" ]
+
+ Thing twinguard tg-corridor "Twinguard Smoke Detector" [ id="hdm:ZigBee:000d6f000XXXXXXX" ]
+ Thing window-contact window-kitchen "Window Kitchen" [ id="hdm:HomeMaticIP:3014F711A00000XXXXXXXXXX" ]
+ Thing window-contact entrance "Entrance door" [ id="hdm:HomeMaticIP:3014F711A00000XXXXXXXXXX" ]
+
+ Thing motion-detector motion-corridor "Bewegungsmelder" [ id="hdm:ZigBee:000d6f000XXXXXXX" ]
+}
+```
+
+Or by adding them via UI: Settings -> Things -> "+" -> Bosch Smart Home Binding.
+
+## Item Configuration
+
+You define the items which should be linked to your Bosch devices via a `.items` file in your `$OPENHAB_CONF/items` folder like this:
+
+```
+Switch Bosch_Bathroom "Bath Room" { channel="boschshc:in-wall-switch:1:bathroom:power-switch" }
+Switch Bosch_Bedroom "Bed Room" { channel="boschshc:in-wall-switch:1:bedroom:power-switch" }
+Switch Bosch_Kitchen "Kitchen" { channel="boschshc:in-wall-switch:1:kitchen:power-switch" }
+Switch Bosch_Corridor "Corridor" { channel="boschshc:in-wall-switch:1:corridor:power-switch" }
+Switch Bosch_Living_Room "Living Room" { channel="boschshc:in-wall-switch:1:livingroom:power-switch" }
+
+Switch Bosch_Lelit "Lelit" { channel="boschshc:in-wall-switch:1:coffeemachine:power-switch" }
+```
+
+Or by adding them via UI: Settings -> Items -> "+".
diff --git a/bundles/org.openhab.binding.boschshc/pom.xml b/bundles/org.openhab.binding.boschshc/pom.xml
new file mode 100644
index 000000000..b85d42a45
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/pom.xml
@@ -0,0 +1,32 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.1.0-SNAPSHOT
+
+
+ org.openhab.binding.boschshc
+
+ openHAB Add-ons :: Bundles :: BoschSHC Binding
+
+
+
+ org.bouncycastle
+ bcpkix-jdk15on
+ 1.52
+ compile
+
+
+ org.bouncycastle
+ bcprov-jdk15on
+ 1.52
+ compile
+
+
+
+
diff --git a/bundles/org.openhab.binding.boschshc/src/main/feature/feature.xml b/bundles/org.openhab.binding.boschshc/src/main/feature/feature.xml
new file mode 100644
index 000000000..314d44d31
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.boschshc/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java
new file mode 100644
index 000000000..47e85f9fc
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java
@@ -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";
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCConfiguration.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCConfiguration.java
new file mode 100644
index 000000000..f1ed032b6
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCConfiguration.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java
new file mode 100644
index 000000000..a683a1f5d
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java
@@ -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 {
+ /**
+ * Constructor.
+ *
+ * @param service Service which belongs to the device.
+ * @param affectedChannels Channels which are affected by the state of this service.
+ */
+ public DeviceService(BoschSHCService service, Collection affectedChannels) {
+ this.service = service;
+ this.affectedChannels = affectedChannels;
+ }
+
+ /**
+ * Service which belongs to the device.
+ */
+ public final BoschSHCService service;
+
+ /**
+ * Channels which are affected by the state of this service.
+ */
+ public final Collection 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> 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 @Nullable T getState(String stateName, Class 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 Type of service.
+ * @param 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 , TState extends BoschSHCServiceState> TService createService(
+ Supplier newService, Consumer stateUpdateListener, Collection affectedChannels)
+ throws BoschSHCException {
+ TService service = newService.get();
+ this.registerService(service, stateUpdateListener, affectedChannels);
+ return service;
+ }
+
+ /**
+ * Registers a service for this device.
+ *
+ * @param Type of service.
+ * @param 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 , TState extends BoschSHCServiceState> void registerService(
+ TService service, Consumer stateUpdateListener, Collection 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 Type of service.
+ * @param Type of service state.
+ * @param service Service to set state for.
+ * @param state State to set.
+ */
+ protected , 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 void registerService(BoschSHCService service,
+ Collection affectedChannels) {
+ this.services.add(new DeviceService(service, affectedChannels));
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java
new file mode 100644
index 000000000..19a1e9419
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java
@@ -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 handlerSupplier;
+
+ public ThingTypeHandlerMapping(ThingTypeUID thingTypeUID, Function handlerSupplier) {
+ this.thingTypeUID = thingTypeUID;
+ this.handlerSupplier = handlerSupplier;
+ }
+ }
+
+ private static final Collection 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);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java
new file mode 100644
index 000000000..e5609117e
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java
@@ -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 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 sendRequest(Request request, Class 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());
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeConfiguration.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeConfiguration.java
new file mode 100644
index 000000000..3943f1432
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeConfiguration.java
@@ -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 = "";
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeHandler.java
new file mode 100644
index 000000000..c5615401a
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeHandler.java
@@ -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>() {
+ }.getType();
+ ArrayList 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>() {
+ }.getType();
+
+ ArrayList 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 @Nullable T getState(String deviceId, String stateName, Class 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 @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;
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSslUtil.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSslUtil.java
new file mode 100644
index 000000000..f23918f69
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSslUtil.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/JsonRpcRequest.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/JsonRpcRequest.java
new file mode 100644
index 000000000..ce17860db
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/JsonRpcRequest.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java
new file mode 100644
index 000000000..0c35b91b2
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java
@@ -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 handleResult;
+
+ /**
+ * Handler for unrecoverable.
+ */
+ private final Consumer 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 handleResult,
+ Consumer 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 {
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Device.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Device.java
new file mode 100644
index 000000000..562e075ed
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Device.java
@@ -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 deviceSerivceIDs;
+ public String manufacturer;
+ public String roomId;
+ public String deviceModel;
+ public String serial;
+ public String profile;
+ public String name;
+ public String status;
+ public List childDeviceIds;
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/DeviceStatusUpdate.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/DeviceStatusUpdate.java
new file mode 100644
index 000000000..649f3de5b
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/DeviceStatusUpdate.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollError.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollError.java
new file mode 100644
index 000000000..ca7df8cee
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollError.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResult.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResult.java
new file mode 100644
index 000000000..3a6bf2b4d
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResult.java
@@ -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 result;
+ public String jsonrpc;
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Room.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Room.java
new file mode 100644
index 000000000..fbfb62486
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Room.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/SubscribeResult.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/SubscribeResult.java
new file mode 100644
index 000000000..c0df59e65
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/SubscribeResult.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/climatecontrol/ClimateControlHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/climatecontrol/ClimateControlHandler.java
new file mode 100644
index 000000000..1b6a96a09
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/climatecontrol/ClimateControlHandler.java
@@ -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));
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/inwallswitch/BoschInWallSwitchHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/inwallswitch/BoschInWallSwitchHandler.java
new file mode 100644
index 000000000..b5a4ebd44
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/inwallswitch/BoschInWallSwitchHandler.java
@@ -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(state.powerConsumption, Units.WATT));
+ updateState(CHANNEL_ENERGY_CONSUMPTION, new QuantityType(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);
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/inwallswitch/dto/PowerMeterState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/inwallswitch/dto/PowerMeterState.java
new file mode 100644
index 000000000..9e0ba54f8
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/inwallswitch/dto/PowerMeterState.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/MotionDetectorHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/MotionDetectorHandler.java
new file mode 100644
index 000000000..6e3f564fe
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/MotionDetectorHandler.java
@@ -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);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/dto/LatestMotionState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/dto/LatestMotionState.java
new file mode 100644
index 000000000..38658342c
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/dto/LatestMotionState.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/shuttercontrol/ShutterControlHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/shuttercontrol/ShutterControlHandler.java
new file mode 100644
index 000000000..6b5450374
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/shuttercontrol/ShutterControlHandler.java
@@ -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));
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandler.java
new file mode 100644
index 000000000..5f7104055
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandler.java
@@ -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());
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/BoschTwinguardHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/BoschTwinguardHandler.java
new file mode 100644
index 000000000..d5d583120
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/BoschTwinguardHandler.java
@@ -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(state.temperature, SIUnits.CELSIUS));
+ updateState(CHANNEL_TEMPERATURE_RATING, new StringType(state.temperatureRating));
+ updateState(CHANNEL_HUMIDITY, new QuantityType(state.humidity, Units.ONE));
+ updateState(CHANNEL_HUMIDITY_RATING, new StringType(state.humidityRating));
+ updateState(CHANNEL_PURITY, new QuantityType(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);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/dto/AirQualityLevelState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/dto/AirQualityLevelState.java
new file mode 100644
index 000000000..724377b53
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/dto/AirQualityLevelState.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/windowcontact/WindowContactHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/windowcontact/WindowContactHandler.java
new file mode 100644
index 000000000..90b978a20
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/windowcontact/WindowContactHandler.java
@@ -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);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/BoschSHCException.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/BoschSHCException.java
new file mode 100644
index 000000000..74fa72e33
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/BoschSHCException.java
@@ -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);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/LongPollingFailedException.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/LongPollingFailedException.java
new file mode 100644
index 000000000..72ae10365
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/LongPollingFailedException.java
@@ -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);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/PairingFailedException.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/PairingFailedException.java
new file mode 100644
index 000000000..50cdfaf36
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/PairingFailedException.java
@@ -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);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/BoschSHCService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/BoschSHCService.java
new file mode 100644
index 000000000..b58d1c324
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/BoschSHCService.java
@@ -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 {
+
+ private final Logger logger = LoggerFactory.getLogger(BoschSHCService.class);
+
+ /**
+ * Unique service name
+ */
+ private final String serviceName;
+
+ /**
+ * Class of service state
+ */
+ private final Class 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 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 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 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 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 stateUpdateListener = this.stateUpdateListener;
+ if (stateUpdateListener != null) {
+ stateUpdateListener.accept(state);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceState.java
new file mode 100644
index 000000000..0ffbfaf14
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceState.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/JsonRestExceptionResponse.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/JsonRestExceptionResponse.java
new file mode 100644
index 000000000..cb8994bad
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/JsonRestExceptionResponse.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchService.java
new file mode 100644
index 000000000..622fac800
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchService.java
@@ -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 {
+
+ public PowerSwitchService() {
+ super("PowerSwitch", PowerSwitchServiceState.class);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchState.java
new file mode 100644
index 000000000..236bcf615
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchState.java
@@ -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
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/dto/PowerSwitchServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/dto/PowerSwitchServiceState.java
new file mode 100644
index 000000000..d75a47609
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/dto/PowerSwitchServiceState.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/roomclimatecontrol/RoomClimateControlService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/roomclimatecontrol/RoomClimateControlService.java
new file mode 100644
index 000000000..b84b39dde
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/roomclimatecontrol/RoomClimateControlService.java
@@ -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 {
+ public RoomClimateControlService() {
+ super("RoomClimateControl", RoomClimateControlServiceState.class);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/roomclimatecontrol/dto/RoomClimateControlServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/roomclimatecontrol/dto/RoomClimateControlServiceState.java
new file mode 100644
index 000000000..bd5a261d6
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/roomclimatecontrol/dto/RoomClimateControlServiceState.java
@@ -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(this.setpointTemperature, SIUnits.CELSIUS);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/ShutterContactService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/ShutterContactService.java
new file mode 100644
index 000000000..743c5031a
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/ShutterContactService.java
@@ -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 {
+ public ShutterContactService() {
+ super("ShutterContact", ShutterContactServiceState.class);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/ShutterContactState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/ShutterContactState.java
new file mode 100644
index 000000000..9391a56f5
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/ShutterContactState.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/dto/ShutterContactServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/dto/ShutterContactServiceState.java
new file mode 100644
index 000000000..7fccf982d
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/dto/ShutterContactServiceState.java
@@ -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");
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/OperationState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/OperationState.java
new file mode 100644
index 000000000..0579b0e5d
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/OperationState.java
@@ -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;
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/ShutterControlService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/ShutterControlService.java
new file mode 100644
index 000000000..ac7ebe34f
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/ShutterControlService.java
@@ -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 {
+ public ShutterControlService() {
+ super("ShutterControl", ShutterControlServiceState.class);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/dto/ShutterControlServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/dto/ShutterControlServiceState.java
new file mode 100644
index 000000000..c7eb5dbcf
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/dto/ShutterControlServiceState.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperaturelevel/TemperatureLevelService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperaturelevel/TemperatureLevelService.java
new file mode 100644
index 000000000..fa66f8a7b
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperaturelevel/TemperatureLevelService.java
@@ -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 {
+ public TemperatureLevelService() {
+ super("TemperatureLevel", TemperatureLevelServiceState.class);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperaturelevel/dto/TemperatureLevelServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperaturelevel/dto/TemperatureLevelServiceState.java
new file mode 100644
index 000000000..415013496
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperaturelevel/dto/TemperatureLevelServiceState.java
@@ -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(this.temperature, SIUnits.CELSIUS);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/valvetappet/ValveTappetService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/valvetappet/ValveTappetService.java
new file mode 100644
index 000000000..0137e48a5
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/valvetappet/ValveTappetService.java
@@ -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 {
+ public ValveTappetService() {
+ super("ValveTappet", ValveTappetServiceState.class);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/valvetappet/dto/ValveTappetServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/valvetappet/dto/ValveTappetServiceState.java
new file mode 100644
index 000000000..3c7e856e0
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/valvetappet/dto/ValveTappetServiceState.java
@@ -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);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 000000000..e7346d106
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,10 @@
+
+
+
+ Bosch Smart Home Binding
+ This is the binding for Bosch Smart Home Controller.
+ Stefan Kästle
+
+
diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/config/configs.xml b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/config/configs.xml
new file mode 100644
index 000000000..0dbe7d94a
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/config/configs.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ network-address
+
+ Network address of the Bosch Smart Home Controller.
+
+
+
+ password
+ The system password of the Bosch Smart Home Controller necessary for pairing.
+
+
+
+
+
+ Unique ID of the device.
+
+
+
diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties
new file mode 100644
index 000000000..e25fee309
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties
@@ -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.
diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc_de.properties b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc_de.properties
new file mode 100644
index 000000000..0ef037c73
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc_de.properties
@@ -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.
diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 000000000..35f4eb5cd
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,262 @@
+
+
+
+
+
+
+ The Bosch SHC Bridge representing the Bosch Smart Home Controller.
+
+
+
+
+
+
+
+
+
+
+ Bosch In-wall switch for light control
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bosch TwinGuard environmental sensor
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bosch Contact for windows and doors
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bosch Motion Detector
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bosch Shutter Control
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bosch Thermostat
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Bosch Climate Control. This is a virtual device which is automatically created for all rooms that have
+ thermostats in it.
+
+
+
+
+
+
+
+
+
+
+
+ Number:Temperature
+
+ Current measured temperature.
+
+
+
+
+ String
+
+ Rating of the currently measured temperature.
+
+
+
+
+
+
+
+
+
+
+ Number:Dimensionless
+
+ Current measured humidity.
+
+
+
+
+ String
+
+ Rating of current measured humidity.
+
+
+
+
+
+
+
+
+
+
+ Number:Energy
+
+ Energy consumption of the device.
+
+
+
+
+ Number:Power
+
+ Current power consumption of the device.
+
+
+
+
+ Number:Dimensionless
+
+ Purity of the air. A higher value indicates a higher pollution.
+
+
+
+
+ String
+
+ Overall description of the air quality.
+
+
+
+
+ String
+
+ Rating of the air purity.
+
+
+
+
+ String
+
+ Combined rating of the air quality.
+
+
+
+
+
+
+
+
+
+
+ Contact
+
+ A window and door contact.
+
+
+
+
+ DateTime
+
+ Timestamp of the latest motion.
+
+
+
+
+ Rollershutter
+
+ Current open ratio (0 to 100).
+
+
+
+
+ Number:Dimensionless
+
+ Current open ratio (0 to 100).
+
+
+
+
+ Number:Temperature
+
+ Desired temperature.
+
+
+
+
diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/org/openhab/binding/boschshc/internal/devices/bridge/SmartHomeControllerIssuingCA.pem b/bundles/org.openhab.binding.boschshc/src/main/resources/org/openhab/binding/boschshc/internal/devices/bridge/SmartHomeControllerIssuingCA.pem
new file mode 100644
index 000000000..25eb08c06
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/resources/org/openhab/binding/boschshc/internal/devices/bridge/SmartHomeControllerIssuingCA.pem
@@ -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-----
diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/org/openhab/binding/boschshc/internal/devices/bridge/SmartHomeControllerProductiveRootCA.pem b/bundles/org.openhab.binding.boschshc/src/main/resources/org/openhab/binding/boschshc/internal/devices/bridge/SmartHomeControllerProductiveRootCA.pem
new file mode 100644
index 000000000..86edc750d
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/resources/org/openhab/binding/boschshc/internal/devices/bridge/SmartHomeControllerProductiveRootCA.pem
@@ -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-----
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java
new file mode 100644
index 000000000..3bbf82a3a
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java
@@ -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);
+ });
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSslUtilTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSslUtilTest.java
new file mode 100644
index 000000000..3da600fc8
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSslUtilTest.java
@@ -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);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResultTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResultTest.java
new file mode 100644
index 000000000..739ac2c78
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResultTest.java
@@ -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);
+ }
+ }
+}
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 394835617..fcd79be95 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -65,6 +65,7 @@
org.openhab.binding.bluetooth.roaming
org.openhab.binding.bluetooth.ruuvitag
org.openhab.binding.boschindego
+ org.openhab.binding.boschshc
org.openhab.binding.bosesoundtouch
org.openhab.binding.bsblan
org.openhab.binding.bticinosmarther