]> git.basschouten.com Git - openhab-addons.git/commitdiff
[boschshc] Initial contribution - Bindings for Bosch Smart Home devices (#8629)
authorChristian Oeing <mail@oeing.eu>
Sun, 17 Jan 2021 21:20:20 +0000 (22:20 +0100)
committerGitHub <noreply@github.com>
Sun, 17 Jan 2021 21:20:20 +0000 (13:20 -0800)
* Initial code from create_openhab_binding_skeleton.sh

Signed-off-by: Stefan Kaestle <stefan@mad-kow.de>
Signed-off-by: Christian Oeing <christian.oeing@slashgames.org>
Signed-off-by: Gerd Zanker <gerd.zanker@web.de>
Co-authored-by: Stefan Kaestle <stefan@mad-kow.de>
Co-authored-by: Gerd Zanker <gerd.zanker@web.de>
Co-authored-by: Christian Oeing <christian.oeing@scalamat.de>
Co-authored-by: Hilbrand Bouwkamp <hilbrand@h72.nl>
Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
Co-authored-by: Connor Petty <mistercpp2000+gitsignoff@gmail.com>
65 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.boschshc/DEVELOPERS.md [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/README.md [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSslUtil.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/JsonRpcRequest.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Device.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/DeviceStatusUpdate.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollError.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResult.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Room.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/SubscribeResult.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/climatecontrol/ClimateControlHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/inwallswitch/BoschInWallSwitchHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/inwallswitch/dto/PowerMeterState.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/MotionDetectorHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/dto/LatestMotionState.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/shuttercontrol/ShutterControlHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/BoschTwinguardHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/dto/AirQualityLevelState.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/windowcontact/WindowContactHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/BoschSHCException.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/LongPollingFailedException.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/PairingFailedException.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/BoschSHCService.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceState.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/JsonRestExceptionResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchService.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchState.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/dto/PowerSwitchServiceState.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/roomclimatecontrol/RoomClimateControlService.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/roomclimatecontrol/dto/RoomClimateControlServiceState.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/ShutterContactService.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/ShutterContactState.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/dto/ShutterContactServiceState.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/OperationState.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/ShutterControlService.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/dto/ShutterControlServiceState.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperaturelevel/TemperatureLevelService.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperaturelevel/dto/TemperatureLevelServiceState.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/valvetappet/ValveTappetService.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/valvetappet/dto/ValveTappetServiceState.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/config/configs.xml [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc_de.properties [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/resources/org/openhab/binding/boschshc/internal/devices/bridge/SmartHomeControllerIssuingCA.pem [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/resources/org/openhab/binding/boschshc/internal/devices/bridge/SmartHomeControllerProductiveRootCA.pem [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSslUtilTest.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResultTest.java [new file with mode: 0644]
bundles/pom.xml

index 363278450ac177f479a35b5f8dda3fdc67d78adc..870d7f28d76cf40ed4deead15801a0ee5163ab58 100644 (file)
@@ -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
index b3039866c6cffc6cf85ccc6b3b759136b98a9709..c15457a389fad21ed43c0641fd07e4907e1392bf 100644 (file)
       <artifactId>org.openhab.binding.boschindego</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.boschshc</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.bosesoundtouch</artifactId>
diff --git a/bundles/org.openhab.binding.boschshc/DEVELOPERS.md b/bundles/org.openhab.binding.boschshc/DEVELOPERS.md
new file mode 100644 (file)
index 0000000..9ee8b4b
--- /dev/null
@@ -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 <ID>
+    
+
+## 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. 
+
+    <!-- Bosch SHC for debugging -->
+       <Logger level="TRACE" name="org.openhab.binding.boschshc"/>
+
+## 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 (file)
index 0000000..bc35a20
--- /dev/null
@@ -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 (file)
index 0000000..b55abc6
--- /dev/null
@@ -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 (file)
index 0000000..b85d42a
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.boschshc</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: BoschSHC Binding</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcpkix-jdk15on</artifactId>
+      <version>1.52</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.bouncycastle</groupId>
+      <artifactId>bcprov-jdk15on</artifactId>
+      <version>1.52</version>
+      <scope>compile</scope>
+    </dependency>
+  </dependencies>
+
+</project>
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 (file)
index 0000000..314d44d
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.boschshc-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-binding-boschshc" description="BoschSHC Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.boschshc/${project.version}</bundle>
+       </feature>
+</features>
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 (file)
index 0000000..47e85f9
--- /dev/null
@@ -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 (file)
index 0000000..f1ed032
--- /dev/null
@@ -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 (file)
index 0000000..a683a1f
--- /dev/null
@@ -0,0 +1,299 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.devices;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.boschshc.internal.devices.bridge.BoschSHCBridgeHandler;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.services.BoschSHCService;
+import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+
+/**
+ * The {@link BoschSHCHandler} represents Bosch Things. Each type of device
+ * inherits from this abstract thing handler.
+ *
+ * @author Stefan Kästle - Initial contribution
+ * @author Christian Oeing - refactorings of e.g. server registration
+ */
+@NonNullByDefault
+public abstract class BoschSHCHandler extends BaseThingHandler {
+
+    /**
+     * Service State for a Bosch device.
+     */
+    class DeviceService<TState extends BoschSHCServiceState> {
+        /**
+         * Constructor.
+         * 
+         * @param service Service which belongs to the device.
+         * @param affectedChannels Channels which are affected by the state of this service.
+         */
+        public DeviceService(BoschSHCService<TState> service, Collection<String> affectedChannels) {
+            this.service = service;
+            this.affectedChannels = affectedChannels;
+        }
+
+        /**
+         * Service which belongs to the device.
+         */
+        public final BoschSHCService<TState> service;
+
+        /**
+         * Channels which are affected by the state of this service.
+         */
+        public final Collection<String> affectedChannels;
+    }
+
+    /**
+     * Reusable gson instance to convert a class to json string and back in derived classes.
+     */
+    protected static final Gson GSON = new Gson();
+
+    protected final Logger logger = LoggerFactory.getLogger(getClass());
+
+    /**
+     * Bosch SHC configuration loaded from openHAB configuration.
+     */
+    private @Nullable BoschSHCConfiguration config;
+
+    /**
+     * Services of the device.
+     */
+    private List<DeviceService<? extends BoschSHCServiceState>> services = new ArrayList<>();
+
+    public BoschSHCHandler(Thing thing) {
+        super(thing);
+    }
+
+    /**
+     * Returns the unique id of the Bosch device.
+     * 
+     * @return Unique id of the Bosch device.
+     */
+    public @Nullable String getBoschID() {
+        BoschSHCConfiguration config = this.config;
+        if (config != null) {
+            return config.id;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Initializes this handler. Use this method to register all services of the device with
+     * {@link #registerService(BoschSHCService)}.
+     */
+    @Override
+    public void initialize() {
+        this.config = getConfigAs(BoschSHCConfiguration.class);
+
+        try {
+            this.initializeServices();
+
+            // Mark immediately as online - if the bridge is online, the thing is too.
+            this.updateStatus(ThingStatus.ONLINE);
+        } catch (BoschSHCException e) {
+            this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+        }
+    }
+
+    /**
+     * Handles the refresh command of all registered services. Override it to handle custom commands (e.g. to update
+     * states of services).
+     * 
+     * @param channelUID {@link ChannelUID} of the channel to which the command was sent
+     * @param command {@link Command}
+     */
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        if (command instanceof RefreshType) {
+            // Refresh state of services that affect the channel
+            for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
+                if (deviceService.affectedChannels.contains(channelUID.getIdWithoutGroup())) {
+                    try {
+                        deviceService.service.refreshState();
+                    } catch (InterruptedException | TimeoutException | ExecutionException | BoschSHCException e) {
+                        this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+                                String.format("Error when trying to refresh state from service %s: %s",
+                                        deviceService.service.getServiceName(), e.getMessage()));
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Processes an update which is received from the bridge.
+     * 
+     * @param serviceName Name of service the update came from.
+     * @param stateData Current state of device service. Serialized as JSON.
+     */
+    public void processUpdate(String serviceName, JsonElement stateData) {
+        // Check services of device to correctly
+        for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
+            BoschSHCService<? extends BoschSHCServiceState> service = deviceService.service;
+            if (serviceName.equals(service.getServiceName())) {
+                service.onStateUpdate(stateData);
+            }
+        }
+    }
+
+    /**
+     * Should be used by handlers to create their required services.
+     */
+    protected void initializeServices() throws BoschSHCException {
+    }
+
+    /**
+     * Returns the bridge handler for this thing handler.
+     * 
+     * @return Bridge handler for this thing handler. Null if no or an invalid bridge was set in the configuration.
+     * @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set.
+     */
+    protected BoschSHCBridgeHandler getBridgeHandler() throws BoschSHCException {
+        Bridge bridge = this.getBridge();
+        if (bridge == null) {
+            throw new BoschSHCException(String.format("No valid bridge set for %s", this.getThing()));
+        }
+        BoschSHCBridgeHandler bridgeHandler = (BoschSHCBridgeHandler) bridge.getHandler();
+        if (bridgeHandler == null) {
+            throw new BoschSHCException(String.format("Bridge of %s has no valid bridge handler", this.getThing()));
+        }
+        return bridgeHandler;
+    }
+
+    /**
+     * Query the Bosch Smart Home Controller for the state of the service with the specified name.
+     * 
+     * @note Use services instead of directly requesting a state.
+     *
+     * @param stateName Name of the service to query
+     * @param classOfT Class to convert the resulting JSON to
+     */
+    protected <T extends BoschSHCServiceState> @Nullable T getState(String stateName, Class<T> classOfT) {
+        String deviceId = this.getBoschID();
+        if (deviceId == null) {
+            return null;
+        }
+        try {
+            BoschSHCBridgeHandler bridgeHandler = this.getBridgeHandler();
+            return bridgeHandler.getState(deviceId, stateName, classOfT);
+        } catch (InterruptedException | TimeoutException | ExecutionException | BoschSHCException e) {
+            this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+                    String.format("Error when trying to refresh state from service %s: %s", stateName, e.getMessage()));
+            return null;
+        }
+    }
+
+    /**
+     * Creates and registers a new service for this device.
+     * 
+     * @param <TService> Type of service.
+     * @param <TState> Type of service state.
+     * @param newService Supplier function to create a new instance of the service.
+     * @param stateUpdateListener Function to call when a state update was received
+     *            from the device.
+     * @param affectedChannels Channels which are affected by the state of this
+     *            service.
+     * @return Instance of registered service.
+     * @throws BoschSHCException
+     */
+    protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> TService createService(
+            Supplier<TService> newService, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels)
+            throws BoschSHCException {
+        TService service = newService.get();
+        this.registerService(service, stateUpdateListener, affectedChannels);
+        return service;
+    }
+
+    /**
+     * Registers a service for this device.
+     * 
+     * @param <TService> Type of service.
+     * @param <TState> Type of service state.
+     * @param service Service to register.
+     * @param stateUpdateListener Function to call when a state update was received
+     *            from the device.
+     * @param affectedChannels Channels which are affected by the state of this
+     *            service.
+     * @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set.
+     * @throws BoschSHCException If no device id is set.
+     */
+    protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void registerService(
+            TService service, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels)
+            throws BoschSHCException {
+        BoschSHCBridgeHandler bridgeHandler = this.getBridgeHandler();
+
+        String deviceId = this.getBoschID();
+        if (deviceId == null) {
+            throw new BoschSHCException(
+                    String.format("Could not register service for %s, no device id set", this.getThing()));
+        }
+
+        service.initialize(bridgeHandler, deviceId, stateUpdateListener);
+        this.registerService(service, affectedChannels);
+    }
+
+    /**
+     * Updates the state of a device service.
+     * Sets the status of the device to offline if setting the state fails.
+     * 
+     * @param <TService> Type of service.
+     * @param <TState> Type of service state.
+     * @param service Service to set state for.
+     * @param state State to set.
+     */
+    protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void updateServiceState(
+            TService service, TState state) {
+        try {
+            service.setState(state);
+        } catch (InterruptedException | TimeoutException | ExecutionException e) {
+            this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String.format(
+                    "Error when trying to update state for service %s: %s", service.getServiceName(), e.getMessage()));
+        }
+    }
+
+    /**
+     * Registers a service of this device.
+     * 
+     * @param service Service which belongs to this device
+     * @param affectedChannels Channels which are affected by the state of this
+     *            service
+     */
+    private <TState extends BoschSHCServiceState> void registerService(BoschSHCService<TState> service,
+            Collection<String> affectedChannels) {
+        this.services.add(new DeviceService<TState>(service, affectedChannels));
+    }
+}
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 (file)
index 0000000..19a1e94
--- /dev/null
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.devices;
+
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_CLIMATE_CONTROL;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_MOTION_DETECTOR;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SHC;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_THERMOSTAT;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_TWINGUARD;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.boschshc.internal.devices.bridge.BoschSHCBridgeHandler;
+import org.openhab.binding.boschshc.internal.devices.climatecontrol.ClimateControlHandler;
+import org.openhab.binding.boschshc.internal.devices.inwallswitch.BoschInWallSwitchHandler;
+import org.openhab.binding.boschshc.internal.devices.motiondetector.MotionDetectorHandler;
+import org.openhab.binding.boschshc.internal.devices.shuttercontrol.ShutterControlHandler;
+import org.openhab.binding.boschshc.internal.devices.thermostat.ThermostatHandler;
+import org.openhab.binding.boschshc.internal.devices.twinguard.BoschTwinguardHandler;
+import org.openhab.binding.boschshc.internal.devices.windowcontact.WindowContactHandler;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * The {@link BoschSHCHandlerFactory} is responsible for creating things and
+ * thing handlers.
+ *
+ * @author Stefan Kästle - Initial contribution
+ * @author Christian Oeing - Added Shutter Control and ThermostatHandler; refactored handler mapping
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.boschshc", service = ThingHandlerFactory.class)
+public class BoschSHCHandlerFactory extends BaseThingHandlerFactory {
+
+    private static class ThingTypeHandlerMapping {
+        public ThingTypeUID thingTypeUID;
+        public Function<Thing, BaseThingHandler> handlerSupplier;
+
+        public ThingTypeHandlerMapping(ThingTypeUID thingTypeUID, Function<Thing, BaseThingHandler> handlerSupplier) {
+            this.thingTypeUID = thingTypeUID;
+            this.handlerSupplier = handlerSupplier;
+        }
+    }
+
+    private static final Collection<ThingTypeHandlerMapping> SUPPORTED_THING_TYPES = List.of(
+            new ThingTypeHandlerMapping(THING_TYPE_SHC, thing -> new BoschSHCBridgeHandler((Bridge) thing)),
+            new ThingTypeHandlerMapping(THING_TYPE_INWALL_SWITCH, BoschInWallSwitchHandler::new),
+            new ThingTypeHandlerMapping(THING_TYPE_TWINGUARD, BoschTwinguardHandler::new),
+            new ThingTypeHandlerMapping(THING_TYPE_WINDOW_CONTACT, WindowContactHandler::new),
+            new ThingTypeHandlerMapping(THING_TYPE_MOTION_DETECTOR, MotionDetectorHandler::new),
+            new ThingTypeHandlerMapping(THING_TYPE_SHUTTER_CONTROL, ShutterControlHandler::new),
+            new ThingTypeHandlerMapping(THING_TYPE_THERMOSTAT, ThermostatHandler::new),
+            new ThingTypeHandlerMapping(THING_TYPE_CLIMATE_CONTROL, ClimateControlHandler::new));
+
+    @Override
+    public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+        return SUPPORTED_THING_TYPES.stream().anyMatch(mapping -> mapping.thingTypeUID.equals(thingTypeUID));
+    }
+
+    @Override
+    protected @Nullable ThingHandler createHandler(Thing thing) {
+        ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+        // Search for mapping for thing type and return handler for it if found. Otherwise return null.
+        return SUPPORTED_THING_TYPES.stream().filter(mapping -> mapping.thingTypeUID.equals(thingTypeUID)).findFirst()
+                .<@Nullable BaseThingHandler> map(mapping -> mapping.handlerSupplier.apply(thing)).orElse(null);
+    }
+}
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 (file)
index 0000000..e560911
--- /dev/null
@@ -0,0 +1,247 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.devices.bridge;
+
+import static org.eclipse.jetty.http.HttpMethod.GET;
+
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStoreException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * HTTP client using own context with private & Bosch Certs
+ * to pair and connect to the Bosch Smart Home Controller.
+ *
+ * @author Gerd Zanker - Initial contribution
+ */
+@NonNullByDefault
+public class BoschHttpClient extends HttpClient {
+    private static final Gson GSON = new Gson();
+
+    private final Logger logger = LoggerFactory.getLogger(BoschHttpClient.class);
+
+    private final String ipAddress;
+    private final String systemPassword;
+
+    public BoschHttpClient(String ipAddress, String systemPassword, SslContextFactory sslContextFactory) {
+        super(sslContextFactory);
+        this.ipAddress = ipAddress;
+        this.systemPassword = systemPassword;
+    }
+
+    /**
+     * Returns the pairing URL for the Bosch SHC clients, using port 8443.
+     * See https://github.com/BoschSmartHome/bosch-shc-api-docs/blob/master/postman/README.md
+     * 
+     * @return URL for pairing
+     */
+    public String getPairingUrl() {
+        return String.format("https://%s:8443/smarthome/clients", this.ipAddress);
+    }
+
+    /**
+     * Returns a Bosch SHC URL for the endpoint, using port 8444.
+     * 
+     * @param endpoint a endpoint, see https://apidocs.bosch-smarthome.com/local/index.html
+     * @return Bosch SHC URL for passed endpoint
+     */
+    public String getBoschShcUrl(String endpoint) {
+        return String.format("https://%s:8444/%s", this.ipAddress, endpoint);
+    }
+
+    /**
+     * Returns a SmartHome URL for the endpoint - shortcut of {@link BoschSslUtil::getBoschShcUrl()}
+     * 
+     * @param endpoint a endpoint, see https://apidocs.bosch-smarthome.com/local/index.html
+     * @return SmartHome URL for passed endpoint
+     */
+    public String getBoschSmartHomeUrl(String endpoint) {
+        return this.getBoschShcUrl(String.format("smarthome/%s", endpoint));
+    }
+
+    /**
+     * Returns a device & service URL.
+     * see https://apidocs.bosch-smarthome.com/local/index.html
+     * 
+     * @param serviceName the name of the service
+     * @param deviceId the device identifier
+     * @return SmartHome URL for passed endpoint
+     */
+    public String getServiceUrl(String serviceName, String deviceId) {
+        return this.getBoschSmartHomeUrl(String.format("devices/%s/services/%s/state", deviceId, serviceName));
+    }
+
+    /**
+     * Checks if the Bosch SHC can be accessed.
+     * 
+     * @return true if HTTP access was successful
+     * @throws InterruptedException in case of an interrupt
+     */
+    public boolean isAccessPossible() throws InterruptedException {
+        try {
+            String url = this.getBoschSmartHomeUrl("devices");
+            Request request = this.createRequest(url, GET);
+            ContentResponse contentResponse = request.send();
+            String content = contentResponse.getContentAsString();
+            logger.debug("Access check response complete: {} - return code: {}", content, contentResponse.getStatus());
+            return true;
+        } catch (TimeoutException | ExecutionException | NullPointerException e) {
+            logger.debug("Access check response failed because of {}!", e.getMessage());
+            return false;
+        }
+    }
+
+    /**
+     * Pairs this client with the Bosch SHC.
+     * Press pairing button on the Bosch Smart Home Controller!
+     * 
+     * @return true if pairing was successful, otherwise false
+     * @throws InterruptedException in case of an interrupt
+     */
+    public boolean doPairing() throws InterruptedException {
+        logger.trace("Starting pairing openHAB Client with Bosch SmartHomeController!");
+        logger.trace("Please press the Bosch SHC button until LED starts blinking");
+
+        ContentResponse contentResponse;
+        try {
+            String publicCert = getCertFromSslContextFactory();
+            logger.trace("Pairing with SHC {}", ipAddress);
+
+            // JSON Rest content
+            Map<String, String> items = new HashMap<>();
+            items.put("@type", "client");
+            items.put("id", BoschSslUtil.getBoschShcClientId()); // Client Id contains the unique OpenHab instance Id
+            items.put("name", "oss_OpenHAB_Binding"); // Client name according to
+                                                      // https://github.com/BoschSmartHome/bosch-shc-api-docs#terms-and-conditions
+            items.put("primaryRole", "ROLE_RESTRICTED_CLIENT");
+            items.put("certificate", "-----BEGIN CERTIFICATE-----\r" + publicCert + "\r-----END CERTIFICATE-----");
+
+            String url = this.getPairingUrl();
+            Request request = this.createRequest(url, HttpMethod.POST, items).header("Systempassword",
+                    Base64.getEncoder().encodeToString(this.systemPassword.getBytes(StandardCharsets.UTF_8)));
+
+            contentResponse = request.send();
+
+            logger.trace("Pairing response complete: {} - return code: {}", contentResponse.getContentAsString(),
+                    contentResponse.getStatus());
+            if (201 == contentResponse.getStatus()) {
+                logger.debug("Pairing successful.");
+                return true;
+            } else {
+                logger.info("Pairing failed with response status {}.", contentResponse.getStatus());
+                return false;
+            }
+        } catch (TimeoutException | CertificateEncodingException | KeyStoreException | NullPointerException e) {
+            logger.warn("Pairing failed with exception {}", e.getMessage());
+            return false;
+        } catch (ExecutionException e) {
+            // javax.net.ssl.SSLHandshakeException: General SSLEngine problem
+            // => usually the pairing failed, because hardware button was not pressed.
+            logger.trace("Pairing failed - Details: {}", e.getMessage());
+            logger.warn("Pairing failed. Was the Bosch SHC button pressed?");
+            return false;
+        }
+    }
+
+    /**
+     * Creates a HTTP request.
+     * 
+     * @param url for the HTTP request
+     * @param method for the HTTP request
+     * @return created HTTP request instance
+     */
+    public Request createRequest(String url, HttpMethod method) {
+        return this.createRequest(url, method, null);
+    }
+
+    /**
+     * Creates a HTTP request.
+     * 
+     * @param url for the HTTP request
+     * @param method for the HTTP request
+     * @param content for the HTTP request
+     * @return created HTTP request instance
+     */
+    public Request createRequest(String url, HttpMethod method, @Nullable Object content) {
+        Request request = this.newRequest(url).method(method).header("Content-Type", "application/json");
+        if (content != null) {
+            String body = GSON.toJson(content);
+            logger.trace("create request for {} and content {}", url, body);
+            request = request.content(new StringContentProvider(body));
+        } else {
+            logger.trace("create request for {}", url);
+        }
+
+        // Set default timeout
+        request.timeout(10, TimeUnit.SECONDS);
+
+        return request;
+    }
+
+    /**
+     * Sends a request and expects a response of the specified type.
+     * 
+     * @param request Request to send
+     * @param responseContentClass Type of expected response
+     * @throws ExecutionException in case of invalid HTTP request result
+     * @throws TimeoutException in case of an HTTP request timeout
+     * @throws InterruptedException in case of an interrupt
+     */
+    public <TContent> TContent sendRequest(Request request, Class<TContent> responseContentClass)
+            throws InterruptedException, TimeoutException, ExecutionException {
+        ContentResponse contentResponse = request.send();
+
+        logger.debug("BoschHttpClient: response complete: {} - return code: {}", contentResponse.getContentAsString(),
+                contentResponse.getStatus());
+
+        try {
+            @Nullable
+            TContent content = GSON.fromJson(contentResponse.getContentAsString(), responseContentClass);
+            if (content == null) {
+                throw new ExecutionException(String.format("Received no content in response, expected type %s",
+                        responseContentClass.getName()), null);
+            }
+            return content;
+        } catch (JsonSyntaxException e) {
+            throw new ExecutionException(String.format("Received invalid content in response, expected type %s: %s",
+                    responseContentClass.getName(), e.getMessage()), e);
+        }
+    }
+
+    private String getCertFromSslContextFactory() throws KeyStoreException, CertificateEncodingException {
+        Certificate cert = this.getSslContextFactory().getKeyStore()
+                .getCertificate(BoschSslUtil.getBoschShcServerId(ipAddress));
+        return Base64.getEncoder().encodeToString(cert.getEncoded());
+    }
+}
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 (file)
index 0000000..3943f14
--- /dev/null
@@ -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 (file)
index 0000000..c561540
--- /dev/null
@@ -0,0 +1,410 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.devices.bridge;
+
+import static org.eclipse.jetty.http.HttpMethod.GET;
+import static org.eclipse.jetty.http.HttpMethod.PUT;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Response;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.*;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
+import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException;
+import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
+import org.openhab.binding.boschshc.internal.services.dto.JsonRestExceptionResponse;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseBridgeHandler;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Representation of a connection with a Bosch Smart Home Controller bridge.
+ *
+ * @author Stefan Kästle - Initial contribution
+ * @author Gerd Zanker - added HttpClient with pairing support
+ * @author Christian Oeing - refactorings of e.g. server registration
+ */
+@NonNullByDefault
+public class BoschSHCBridgeHandler extends BaseBridgeHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(BoschSHCBridgeHandler.class);
+
+    /**
+     * gson instance to convert a class to json string and back.
+     */
+    private final Gson gson = new Gson();
+
+    /**
+     * Handler to do long polling.
+     */
+    private final LongPolling longPolling;
+
+    private @Nullable BoschHttpClient httpClient;
+
+    private @Nullable ScheduledFuture<?> scheduledPairing;
+
+    public BoschSHCBridgeHandler(Bridge bridge) {
+        super(bridge);
+
+        this.longPolling = new LongPolling(this.scheduler, this::handleLongPollResult, this::handleLongPollFailure);
+    }
+
+    @Override
+    public void initialize() {
+        // Read configuration
+        BoschSHCBridgeConfiguration config = getConfigAs(BoschSHCBridgeConfiguration.class);
+
+        if (config.ipAddress.isEmpty()) {
+            this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No IP address set");
+            return;
+        }
+
+        if (config.password.isEmpty()) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No system password set");
+            return;
+        }
+
+        SslContextFactory factory;
+        try {
+            // prepare SSL key and certificates
+            factory = new BoschSslUtil(config.ipAddress).getSslContextFactory();
+        } catch (PairingFailedException e) {
+            this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
+                    "@text/offline.conf-error-ssl");
+            return;
+        }
+
+        // Instantiate HttpClient with the SslContextFactory
+        BoschHttpClient httpClient = this.httpClient = new BoschHttpClient(config.ipAddress, config.password, factory);
+
+        // Start http client
+        try {
+            httpClient.start();
+        } catch (Exception e) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+                    String.format("Could not create http connection to controller: %s", e.getMessage()));
+            return;
+        }
+
+        // Initialize bridge in the background.
+        // Start initial access the first time
+        scheduleInitialAccess(httpClient);
+    }
+
+    @Override
+    public void dispose() {
+        // Cancel scheduled pairing.
+        ScheduledFuture<?> scheduledPairing = this.scheduledPairing;
+        if (scheduledPairing != null) {
+            scheduledPairing.cancel(true);
+            this.scheduledPairing = null;
+        }
+
+        // Stop long polling.
+        this.longPolling.stop();
+
+        BoschHttpClient httpClient = this.httpClient;
+        if (httpClient != null) {
+            try {
+                httpClient.stop();
+            } catch (Exception e) {
+                logger.debug("HttpClient failed on bridge disposal: {}", e.getMessage());
+            }
+            this.httpClient = null;
+        }
+
+        super.dispose();
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+    }
+
+    /**
+     * Schedule the initial access.
+     * Use a delay if pairing fails and next retry is scheduled.
+     */
+    private void scheduleInitialAccess(BoschHttpClient httpClient) {
+        this.scheduledPairing = scheduler.schedule(() -> initialAccess(httpClient), 15, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Execute the initial access.
+     * Uses the HTTP Bosch SHC client
+     * to check if access if possible
+     * pairs this Bosch SHC Bridge with the SHC if necessary
+     * and starts the first log poll.
+     */
+    private void initialAccess(BoschHttpClient httpClient) {
+        logger.debug("Initializing Bosch SHC Bridge: {} - HTTP client is: {} - version: 2020-04-05", this, httpClient);
+
+        try {
+            // check access and pair if necessary
+            if (!httpClient.isAccessPossible()) {
+                // update status already if access is not possible
+                this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
+                        "@text/offline.conf-error-pairing");
+                if (!httpClient.doPairing()) {
+                    this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
+                            "@text/offline.conf-error-pairing");
+                }
+                // restart initial access - needed also in case of successful pairing to check access again
+                scheduleInitialAccess(httpClient);
+            } else {
+                // print rooms and devices if things are reachable
+                boolean thingReachable = true;
+                thingReachable &= this.getRooms();
+                thingReachable &= this.getDevices();
+
+                if (thingReachable) {
+                    this.updateStatus(ThingStatus.ONLINE);
+
+                    // Start long polling
+                    try {
+                        this.longPolling.start(httpClient);
+                    } catch (LongPollingFailedException e) {
+                        this.handleLongPollFailure(e);
+                    }
+                } else {
+                    this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+                            "@text/offline.not-reachable");
+                    // restart initial access
+                    scheduleInitialAccess(httpClient);
+                }
+            }
+        } catch (InterruptedException e) {
+            this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
+                    String.format("Pairing was interrupted: %s", e.getMessage()));
+        }
+    }
+
+    /**
+     * Get a list of connected devices from the Smart-Home Controller
+     * 
+     * @throws InterruptedException
+     */
+    private boolean getDevices() throws InterruptedException {
+        BoschHttpClient httpClient = this.httpClient;
+        if (httpClient == null) {
+            return false;
+        }
+
+        try {
+            logger.debug("Sending http request to Bosch to request clients: {}", httpClient);
+            String url = httpClient.getBoschSmartHomeUrl("devices");
+            ContentResponse contentResponse = httpClient.createRequest(url, GET).send();
+
+            String content = contentResponse.getContentAsString();
+            logger.debug("Response complete: {} - return code: {}", content, contentResponse.getStatus());
+
+            Type collectionType = new TypeToken<ArrayList<Device>>() {
+            }.getType();
+            ArrayList<Device> devices = gson.fromJson(content, collectionType);
+
+            if (devices != null) {
+                for (Device d : devices) {
+                    // Write found devices into openhab.log until we have implemented auto discovery
+                    logger.info("Found device: name={} id={}", d.name, d.id);
+                    if (d.deviceSerivceIDs != null) {
+                        for (String s : d.deviceSerivceIDs) {
+                            logger.info(".... service: {}", s);
+                        }
+                    }
+                }
+            }
+        } catch (TimeoutException | ExecutionException e) {
+            logger.debug("HTTP request failed with exception {}", e.getMessage());
+            return false;
+        }
+
+        return true;
+    }
+
+    private void handleLongPollResult(LongPollResult result) {
+        for (DeviceStatusUpdate update : result.result) {
+            if (update != null && update.state != null) {
+                logger.debug("Got update for {}", update.deviceId);
+
+                boolean handled = false;
+
+                Bridge bridge = this.getThing();
+                for (Thing childThing : bridge.getThings()) {
+                    // All children of this should implement BoschSHCHandler
+                    ThingHandler baseHandler = childThing.getHandler();
+                    if (baseHandler != null && baseHandler instanceof BoschSHCHandler) {
+                        BoschSHCHandler handler = (BoschSHCHandler) baseHandler;
+                        String deviceId = handler.getBoschID();
+
+                        handled = true;
+                        logger.debug("Registered device: {} - looking for {}", deviceId, update.deviceId);
+
+                        if (deviceId != null && update.deviceId.equals(deviceId)) {
+                            logger.debug("Found child: {} - calling processUpdate with {}", handler, update.state);
+                            handler.processUpdate(update.id, update.state);
+                        }
+                    } else {
+                        logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler);
+                    }
+                }
+
+                if (!handled) {
+                    logger.debug("Could not find a thing for device ID: {}", update.deviceId);
+                }
+            }
+        }
+    }
+
+    private void handleLongPollFailure(Throwable e) {
+        logger.warn("Long polling failed", e);
+        updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Long polling failed");
+    }
+
+    /**
+     * Get a list of rooms from the Smart-Home controller
+     * 
+     * @throws InterruptedException
+     */
+    private boolean getRooms() throws InterruptedException {
+        BoschHttpClient httpClient = this.httpClient;
+        if (httpClient != null) {
+            try {
+                logger.debug("Sending http request to Bosch to request rooms");
+                String url = httpClient.getBoschSmartHomeUrl("rooms");
+                ContentResponse contentResponse = httpClient.createRequest(url, GET).send();
+
+                String content = contentResponse.getContentAsString();
+                logger.debug("Response complete: {} - return code: {}", content, contentResponse.getStatus());
+
+                Type collectionType = new TypeToken<ArrayList<Room>>() {
+                }.getType();
+
+                ArrayList<Room> rooms = gson.fromJson(content, collectionType);
+
+                if (rooms != null) {
+                    for (Room r : rooms) {
+                        logger.info("Found room: {}", r.name);
+                    }
+                }
+
+                return true;
+            } catch (TimeoutException | ExecutionException e) {
+                logger.warn("HTTP request failed: {}", e.getMessage());
+                return false;
+            }
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Query the Bosch Smart Home Controller for the state of the given thing.
+     *
+     * @param deviceId Id of device to get state for
+     * @param stateName Name of the state to query
+     * @param stateClass Class to convert the resulting JSON to
+     * @throws ExecutionException
+     * @throws TimeoutException
+     * @throws InterruptedException
+     * @throws BoschSHCException
+     */
+    public <T extends BoschSHCServiceState> @Nullable T getState(String deviceId, String stateName, Class<T> stateClass)
+            throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+        BoschHttpClient httpClient = this.httpClient;
+        if (httpClient == null) {
+            logger.warn("HttpClient not initialized");
+            return null;
+        }
+
+        String url = httpClient.getServiceUrl(stateName, deviceId);
+        Request request = httpClient.createRequest(url, GET).header("Accept", "application/json");
+
+        logger.debug("refreshState: Requesting \"{}\" from Bosch: {} via {}", stateName, deviceId, url);
+
+        ContentResponse contentResponse = request.send();
+
+        String content = contentResponse.getContentAsString();
+        logger.debug("refreshState: Request complete: [{}] - return code: {}", content, contentResponse.getStatus());
+
+        int statusCode = contentResponse.getStatus();
+        if (statusCode != 200) {
+            JsonRestExceptionResponse errorResponse = gson.fromJson(content, JsonRestExceptionResponse.class);
+            if (errorResponse != null) {
+                throw new BoschSHCException(String.format(
+                        "State request for service %s of device %s failed with status code %d and error code %s",
+                        stateName, deviceId, errorResponse.statusCode, errorResponse.errorCode));
+            } else {
+                throw new BoschSHCException(
+                        String.format("State request for service %s of device %s failed with status code %d", stateName,
+                                deviceId, statusCode));
+            }
+        }
+
+        @Nullable
+        T state = gson.fromJson(content, stateClass);
+        if (state == null) {
+            throw new BoschSHCException(String.format("Received invalid, expected type %s", stateClass.getName()));
+        }
+        return state;
+    }
+
+    /**
+     * Sends a state change for a device to the controller
+     * 
+     * @param deviceId Id of device to change state for
+     * @param serviceName Name of service of device to change state for
+     * @param state New state data to set for service
+     * 
+     * @return Response of request
+     * @throws InterruptedException
+     * @throws ExecutionException
+     * @throws TimeoutException
+     */
+    public <T extends BoschSHCServiceState> @Nullable Response putState(String deviceId, String serviceName, T state)
+            throws InterruptedException, TimeoutException, ExecutionException {
+        BoschHttpClient httpClient = this.httpClient;
+        if (httpClient == null) {
+            logger.warn("HttpClient not initialized");
+            return null;
+        }
+
+        // Create request
+        String url = httpClient.getServiceUrl(serviceName, deviceId);
+        Request request = httpClient.createRequest(url, PUT, state);
+
+        // Send request
+        Response response = request.send();
+        return response;
+    }
+}
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 (file)
index 0000000..f23918f
--- /dev/null
@@ -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 (file)
index 0000000..ce17860
--- /dev/null
@@ -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 (file)
index 0000000..0c35b91
--- /dev/null
@@ -0,0 +1,211 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.devices.bridge;
+
+import static org.eclipse.jetty.http.HttpMethod.POST;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.api.Result;
+import org.eclipse.jetty.client.util.BufferingResponseListener;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollError;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * Handles the long polling to the Smart Home Controller.
+ * 
+ * @author Christian Oeing - Initial contribution
+ */
+@NonNullByDefault
+public class LongPolling {
+
+    private final Logger logger = LoggerFactory.getLogger(LongPolling.class);
+
+    /**
+     * gson instance to convert a class to json string and back.
+     */
+    private final Gson gson = new Gson();
+
+    /**
+     * Executor to schedule long polls.
+     */
+    private final ScheduledExecutorService scheduler;
+
+    /**
+     * Handler for long poll results.
+     */
+    private final Consumer<LongPollResult> handleResult;
+
+    /**
+     * Handler for unrecoverable.
+     */
+    private final Consumer<Throwable> handleFailure;
+
+    /**
+     * Current running long polling request.
+     */
+    private @Nullable Request request;
+
+    /**
+     * Indicates if long polling was aborted.
+     */
+    private boolean aborted = false;
+
+    public LongPolling(ScheduledExecutorService scheduler, Consumer<LongPollResult> handleResult,
+            Consumer<Throwable> handleFailure) {
+        this.scheduler = scheduler;
+        this.handleResult = handleResult;
+        this.handleFailure = handleFailure;
+    }
+
+    public void start(BoschHttpClient httpClient) throws LongPollingFailedException {
+        // Subscribe to state updates.
+        String subscriptionId = this.subscribe(httpClient);
+        this.executeLongPoll(httpClient, subscriptionId);
+    }
+
+    public void stop() {
+        // Abort long polling.
+        this.aborted = true;
+        Request request = this.request;
+        if (request != null) {
+            request.abort(new AbortLongPolling());
+            this.request = null;
+        }
+    }
+
+    /**
+     * Subscribe to events and store the subscription ID needed for long polling.
+     * 
+     * @param httpClient Http client to use for sending subscription request
+     * @return Subscription id
+     */
+    private String subscribe(BoschHttpClient httpClient) throws LongPollingFailedException {
+        try {
+            String url = httpClient.getBoschShcUrl("remote/json-rpc");
+            JsonRpcRequest request = new JsonRpcRequest("2.0", "RE/subscribe",
+                    new String[] { "com/bosch/sh/remote/*", null });
+            logger.debug("Subscribe: Sending request: {} - using httpClient {}", gson.toJson(request), httpClient);
+            Request httpRequest = httpClient.createRequest(url, POST, request);
+            SubscribeResult response = httpClient.sendRequest(httpRequest, SubscribeResult.class);
+
+            logger.debug("Subscribe: Got subscription ID: {} {}", response.getResult(), response.getJsonrpc());
+            String subscriptionId = response.getResult();
+            return subscriptionId;
+        } catch (TimeoutException | ExecutionException | InterruptedException e) {
+            throw new LongPollingFailedException("Error on subscribe request", e);
+        }
+    }
+
+    private void executeLongPoll(BoschHttpClient httpClient, String subscriptionId) {
+        scheduler.execute(() -> this.longPoll(httpClient, subscriptionId));
+    }
+
+    /**
+     * Start long polling the home controller. Once a long poll resolves, a new one is started.
+     */
+    private void longPoll(BoschHttpClient httpClient, String subscriptionId) {
+        logger.debug("Sending long poll request");
+
+        JsonRpcRequest requestContent = new JsonRpcRequest("2.0", "RE/longPoll", new String[] { subscriptionId, "20" });
+        String url = httpClient.getBoschShcUrl("remote/json-rpc");
+        Request request = httpClient.createRequest(url, POST, requestContent);
+
+        // Long polling responds after 20 seconds with an empty response if no update has happened.
+        // 10 second threshold was added to not time out if response from controller takes a bit longer than 20 seconds.
+        request.timeout(30, TimeUnit.SECONDS);
+
+        this.request = request;
+        LongPolling longPolling = this;
+        request.send(new BufferingResponseListener() {
+            @Override
+            public void onComplete(@Nullable Result result) {
+                Throwable failure = result != null ? result.getFailure() : null;
+                if (failure != null) {
+                    if (failure instanceof ExecutionException) {
+                        if (failure.getCause() instanceof AbortLongPolling) {
+                            logger.debug("Canceling long polling for subscription id {} because it was aborted",
+                                    subscriptionId);
+                        } else {
+                            longPolling.handleFailure.accept(new LongPollingFailedException(
+                                    "Unexpected exception during long polling request", failure));
+                        }
+                    } else {
+                        longPolling.handleFailure.accept(new LongPollingFailedException(
+                                "Unexpected exception during long polling request", failure));
+                    }
+                } else {
+                    longPolling.onLongPollResponse(httpClient, subscriptionId, this.getContentAsString());
+                }
+            }
+        });
+    }
+
+    private void onLongPollResponse(BoschHttpClient httpClient, String subscriptionId, String content) {
+        // Check if thing is still online
+        if (this.aborted) {
+            logger.debug("Canceling long polling for subscription id {} because it was aborted", subscriptionId);
+            return;
+        }
+
+        logger.debug("Long poll response: {}", content);
+
+        String nextSubscriptionId = subscriptionId;
+
+        LongPollResult longPollResult = gson.fromJson(content, LongPollResult.class);
+        if (longPollResult != null && longPollResult.result != null) {
+            this.handleResult.accept(longPollResult);
+        } else {
+            logger.warn("Long poll response contained no results: {}", content);
+
+            // Check if we got a proper result from the SHC
+            LongPollError longPollError = gson.fromJson(content, LongPollError.class);
+
+            if (longPollError != null && longPollError.error != null) {
+                logger.warn("Got long poll error: {} (code: {})", longPollError.error.message,
+                        longPollError.error.code);
+
+                if (longPollError.error.code == LongPollError.SUBSCRIPTION_INVALID) {
+                    logger.warn("Subscription {} became invalid, subscribing again", subscriptionId);
+                    try {
+                        nextSubscriptionId = this.subscribe(httpClient);
+                    } catch (LongPollingFailedException e) {
+                        this.handleFailure.accept(e);
+                        return;
+                    }
+                }
+            }
+        }
+
+        // Execute next run.
+        this.executeLongPoll(httpClient, nextSubscriptionId);
+    }
+
+    @SuppressWarnings("serial")
+    private class AbortLongPolling extends BoschSHCException {
+    }
+}
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 (file)
index 0000000..562e075
--- /dev/null
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.devices.bridge.dto;
+
+import java.util.List;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Represents a single devices connected to the Bosch Smart Home Controller.
+ *
+ * Example from Json:
+ *
+ * {
+ * "@type":"device",
+ * "rootDeviceId":"64-da-a0-02-14-9b",
+ * "id":"hdm:HomeMaticIP:3014F711A00004953859F31B",
+ * "deviceServiceIds":["PowerMeter","PowerSwitch","PowerSwitchProgram","Routing"],
+ * "manufacturer":"BOSCH",
+ * "roomId":"hz_3",
+ * "deviceModel":"PSM",
+ * "serial":"3014F711A00004953859F31B",
+ * "profile":"GENERIC",
+ * "name":"Coffee Machine",
+ * "status":"AVAILABLE",
+ * "childDeviceIds":[]
+ * }
+ *
+ * @author Stefan Kästle - Initial contribution
+ */
+public class Device {
+
+    @SerializedName("@type")
+    public String type;
+
+    public String rootDeviceId;
+    public String id;
+    public List<String> deviceSerivceIDs;
+    public String manufacturer;
+    public String roomId;
+    public String deviceModel;
+    public String serial;
+    public String profile;
+    public String name;
+    public String status;
+    public List<String> childDeviceIds;
+}
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 (file)
index 0000000..649f3de
--- /dev/null
@@ -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 (file)
index 0000000..ca7df8c
--- /dev/null
@@ -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 (file)
index 0000000..3a6bf2b
--- /dev/null
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.devices.bridge.dto;
+
+import java.util.ArrayList;
+
+/**
+ * Response of the Controller for a Long Poll API call.
+ *
+ * @author Stefan Kästle - Initial contribution
+ */
+public class LongPollResult {
+
+    /**
+     * {"result":[
+     * ..{
+     * ...."path":"/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch",
+     * ...."@type":"DeviceServiceData",
+     * ...."id":"PowerSwitch",
+     * ...."state":{
+     * ......"@type":"powerSwitchState",
+     * ......"switchState":"ON"
+     * ....},
+     * ...."deviceId":"hdm:HomeMaticIP:3014F711A0001916D859A8A9"}
+     * ],"jsonrpc":"2.0"}
+     */
+
+    public ArrayList<DeviceStatusUpdate> result;
+    public String jsonrpc;
+}
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 (file)
index 0000000..fbfb624
--- /dev/null
@@ -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 (file)
index 0000000..c0df59e
--- /dev/null
@@ -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 (file)
index 0000000..1b6a96a
--- /dev/null
@@ -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 (file)
index 0000000..b5a4ebd
--- /dev/null
@@ -0,0 +1,133 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.devices.inwallswitch;
+
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*;
+
+import java.util.List;
+
+import javax.measure.quantity.Energy;
+import javax.measure.quantity.Power;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
+import org.openhab.binding.boschshc.internal.devices.inwallswitch.dto.PowerMeterState;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchService;
+import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState;
+import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+
+import com.google.gson.JsonElement;
+
+/**
+ * Represents Bosch in-wall switches.
+ *
+ * @author Stefan Kästle - Initial contribution
+ */
+@NonNullByDefault
+public class BoschInWallSwitchHandler extends BoschSHCHandler {
+
+    private final PowerSwitchService powerSwitchService;
+
+    public BoschInWallSwitchHandler(Thing thing) {
+        super(thing);
+        this.powerSwitchService = new PowerSwitchService();
+    }
+
+    @Override
+    protected void initializeServices() throws BoschSHCException {
+        super.initializeServices();
+
+        this.registerService(this.powerSwitchService, this::updateChannels, List.of(CHANNEL_POWER_SWITCH));
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        super.handleCommand(channelUID, command);
+
+        logger.debug("Handle command for: {} - {}", channelUID.getThingUID(), command);
+
+        if (command instanceof RefreshType) {
+            switch (channelUID.getId()) {
+                case CHANNEL_POWER_CONSUMPTION: {
+                    PowerMeterState state = this.getState("PowerMeter", PowerMeterState.class);
+                    if (state != null) {
+                        updatePowerMeterState(state);
+                    }
+                    break;
+                }
+                case CHANNEL_ENERGY_CONSUMPTION:
+                    // Nothing to do here, since the same update is received from POWER_CONSUMPTION
+                    break;
+                default:
+                    logger.warn("Received refresh request for unsupported channel: {}", channelUID);
+            }
+        } else {
+            switch (channelUID.getId()) {
+                case CHANNEL_POWER_SWITCH:
+                    if (command instanceof OnOffType) {
+                        updatePowerSwitchState((OnOffType) command);
+                    }
+                    break;
+            }
+        }
+    }
+
+    void updatePowerMeterState(PowerMeterState state) {
+        logger.debug("Parsed power meter state of {}: energy {} - power {}", this.getBoschID(), state.energyConsumption,
+                state.energyConsumption);
+
+        updateState(CHANNEL_POWER_CONSUMPTION, new QuantityType<Power>(state.powerConsumption, Units.WATT));
+        updateState(CHANNEL_ENERGY_CONSUMPTION, new QuantityType<Energy>(state.energyConsumption, Units.WATT_HOUR));
+    }
+
+    /**
+     * Updates the channels which are linked to the {@link PowerSwitchService} of the device.
+     * 
+     * @param state Current state of {@link PowerSwitchService}.
+     */
+    private void updateChannels(PowerSwitchServiceState state) {
+        State powerState = OnOffType.from(state.switchState.toString());
+        super.updateState(CHANNEL_POWER_SWITCH, powerState);
+    }
+
+    private void updatePowerSwitchState(OnOffType command) {
+        PowerSwitchServiceState state = new PowerSwitchServiceState();
+        state.switchState = PowerSwitchState.valueOf(command.toFullString());
+        this.updateServiceState(this.powerSwitchService, state);
+    }
+
+    @Override
+    public void processUpdate(String id, JsonElement state) {
+        super.processUpdate(id, state);
+
+        logger.debug("in-wall switch: received update: ID {} state {}", id, state);
+
+        if (id.equals("PowerMeter")) {
+            PowerMeterState powerMeterState = GSON.fromJson(state, PowerMeterState.class);
+            if (powerMeterState == null) {
+                logger.warn("Received unknown update in in-wall switch: {}", state);
+            } else {
+                updatePowerMeterState(powerMeterState);
+            }
+        }
+    }
+}
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 (file)
index 0000000..9e0ba54
--- /dev/null
@@ -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 (file)
index 0000000..6e3f564
--- /dev/null
@@ -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 (file)
index 0000000..3865834
--- /dev/null
@@ -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 (file)
index 0000000..6b54503
--- /dev/null
@@ -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 (file)
index 0000000..5f71040
--- /dev/null
@@ -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 (file)
index 0000000..d5d5831
--- /dev/null
@@ -0,0 +1,92 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.devices.twinguard;
+
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*;
+
+import javax.measure.quantity.Dimensionless;
+import javax.measure.quantity.Temperature;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
+import org.openhab.binding.boschshc.internal.devices.twinguard.dto.AirQualityLevelState;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.Bridge;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link BoschSHCHandler} is responsible for handling commands for the TwinGuard handler.
+ *
+ * @author Stefan Kästle - Initial contribution
+ */
+@NonNullByDefault
+public class BoschTwinguardHandler extends BoschSHCHandler {
+
+    public BoschTwinguardHandler(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        Bridge bridge = this.getBridge();
+
+        if (bridge != null) {
+            logger.debug("Handle command for: {} - {}", channelUID.getThingUID(), command);
+
+            if (command instanceof RefreshType) {
+                AirQualityLevelState state = this.getState("AirQualityLevel", AirQualityLevelState.class);
+                if (state != null) {
+                    updateAirQualityState(state);
+                }
+            }
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Bridge is NUL");
+        }
+    }
+
+    void updateAirQualityState(AirQualityLevelState state) {
+        updateState(CHANNEL_TEMPERATURE, new QuantityType<Temperature>(state.temperature, SIUnits.CELSIUS));
+        updateState(CHANNEL_TEMPERATURE_RATING, new StringType(state.temperatureRating));
+        updateState(CHANNEL_HUMIDITY, new QuantityType<Dimensionless>(state.humidity, Units.ONE));
+        updateState(CHANNEL_HUMIDITY_RATING, new StringType(state.humidityRating));
+        updateState(CHANNEL_PURITY, new QuantityType<Dimensionless>(state.purity, Units.ONE));
+        updateState(CHANNEL_AIR_DESCRIPTION, new StringType(state.description));
+        updateState(CHANNEL_PURITY_RATING, new StringType(state.purityRating));
+        updateState(CHANNEL_COMBINED_RATING, new StringType(state.combinedRating));
+    }
+
+    @Override
+    public void processUpdate(String id, JsonElement state) throws JsonSyntaxException {
+        logger.debug("Twinguard: received update: {} {}", id, state);
+
+        AirQualityLevelState parsed = GSON.fromJson(state, AirQualityLevelState.class);
+        if (parsed == null) {
+            logger.warn("Received unknown update in in-wall switch: {}", state);
+            return;
+        }
+
+        logger.debug("Parsed switch state of {}: {}", this.getBoschID(), parsed);
+        updateAirQualityState(parsed);
+    }
+}
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 (file)
index 0000000..724377b
--- /dev/null
@@ -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 (file)
index 0000000..90b978a
--- /dev/null
@@ -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 (file)
index 0000000..74fa72e
--- /dev/null
@@ -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 (file)
index 0000000..72ae103
--- /dev/null
@@ -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 (file)
index 0000000..50cdfaf
--- /dev/null
@@ -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 (file)
index 0000000..b58d1c3
--- /dev/null
@@ -0,0 +1,198 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.services;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.boschshc.internal.devices.bridge.BoschSHCBridgeHandler;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+
+/**
+ * Base class of a service of a Bosch Smart Home device.
+ * The services of the devices and their official APIs can be found here: https://apidocs.bosch-smarthome.com/local/
+ * 
+ * @author Christian Oeing - Initial contribution
+ */
+@NonNullByDefault
+public abstract class BoschSHCService<TState extends BoschSHCServiceState> {
+
+    private final Logger logger = LoggerFactory.getLogger(BoschSHCService.class);
+
+    /**
+     * Unique service name
+     */
+    private final String serviceName;
+
+    /**
+     * Class of service state
+     */
+    private final Class<TState> stateClass;
+
+    /**
+     * gson instance to convert a class to json string and back.
+     */
+    private final Gson gson = new Gson();
+
+    /**
+     * Bridge to use for communication from/to the device
+     */
+    private @Nullable BoschSHCBridgeHandler bridgeHandler;
+
+    /**
+     * Id of device the service belongs to
+     */
+    private @Nullable String deviceId;
+
+    /**
+     * Function to call after receiving state updates from the device
+     */
+    private @Nullable Consumer<TState> stateUpdateListener;
+
+    /**
+     * Constructor
+     * 
+     * @param serviceName Unique name of the service.
+     * @param stateClass State class that this service uses for data transfers from/to the device.
+     */
+    protected BoschSHCService(String serviceName, Class<TState> stateClass) {
+        this.serviceName = serviceName;
+        this.stateClass = stateClass;
+    }
+
+    /**
+     * Initializes the service
+     * 
+     * @param bridgeHandler Bridge to use for communication from/to the device
+     * @param deviceId Id of device this service is for
+     * @param stateUpdateListener Function to call when a state update was received from the device.
+     */
+    public void initialize(BoschSHCBridgeHandler bridgeHandler, String deviceId,
+            @Nullable Consumer<TState> stateUpdateListener) {
+        this.bridgeHandler = bridgeHandler;
+        this.deviceId = deviceId;
+        this.stateUpdateListener = stateUpdateListener;
+    }
+
+    /**
+     * Returns the unique name of this service.
+     * 
+     * @return Unique name of the service.
+     */
+    public String getServiceName() {
+        return this.serviceName;
+    }
+
+    /**
+     * Returns the class of the state this service provides.
+     * 
+     * @return Class of the state this service provides.
+     */
+    public Class<TState> getStateClass() {
+        return this.stateClass;
+    }
+
+    /**
+     * Requests the current state of the service and updates it.
+     * 
+     * @throws ExecutionException
+     * @throws TimeoutException
+     * @throws InterruptedException
+     * @throws BoschSHCException
+     */
+    public void refreshState() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+        @Nullable
+        TState state = this.getState();
+        if (state != null) {
+            this.onStateUpdate(state);
+        }
+    }
+
+    /**
+     * Requests the current state of the device with the specified id.
+     * 
+     * @return Current state of the device.
+     * @throws ExecutionException
+     * @throws TimeoutException
+     * @throws InterruptedException
+     * @throws BoschSHCException
+     */
+    public @Nullable TState getState()
+            throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+        String deviceId = this.deviceId;
+        if (deviceId == null) {
+            return null;
+        }
+        BoschSHCBridgeHandler bridgeHandler = this.bridgeHandler;
+        if (bridgeHandler == null) {
+            return null;
+        }
+        return bridgeHandler.getState(deviceId, this.serviceName, this.stateClass);
+    }
+
+    /**
+     * Sets the state of the device with the specified id.
+     * 
+     * @param state State to set.
+     * @throws InterruptedException
+     * @throws ExecutionException
+     * @throws TimeoutException
+     */
+    public void setState(TState state) throws InterruptedException, TimeoutException, ExecutionException {
+        String deviceId = this.deviceId;
+        if (deviceId == null) {
+            return;
+        }
+        BoschSHCBridgeHandler bridgeHandler = this.bridgeHandler;
+        if (bridgeHandler == null) {
+            return;
+        }
+        bridgeHandler.putState(deviceId, this.serviceName, state);
+    }
+
+    /**
+     * A state update was received from the bridge
+     * 
+     * @param stateData Current state of service. Serialized as JSON.
+     */
+    public void onStateUpdate(JsonElement stateData) {
+        @Nullable
+        TState state = gson.fromJson(stateData, this.stateClass);
+        if (state == null) {
+            this.logger.warn("Received invalid, expected type {}", this.stateClass.getName());
+            return;
+        }
+        this.onStateUpdate(state);
+    }
+
+    /**
+     * A state update was received from the bridge.
+     * 
+     * @param state Current state of service as an instance of the state class.
+     */
+    private void onStateUpdate(TState state) {
+        Consumer<TState> stateUpdateListener = this.stateUpdateListener;
+        if (stateUpdateListener != null) {
+            stateUpdateListener.accept(state);
+        }
+    }
+}
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 (file)
index 0000000..0ffbfaf
--- /dev/null
@@ -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 (file)
index 0000000..cb8994b
--- /dev/null
@@ -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 (file)
index 0000000..622fac8
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.services.powerswitch;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.services.BoschSHCService;
+import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState;
+
+/**
+ * Service to get and set the state of a power switch.
+ * 
+ * @author Christian Oeing - Initial contribution
+ */
+@NonNullByDefault
+public class PowerSwitchService extends BoschSHCService<PowerSwitchServiceState> {
+
+    public PowerSwitchService() {
+        super("PowerSwitch", PowerSwitchServiceState.class);
+    }
+}
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 (file)
index 0000000..236bcf6
--- /dev/null
@@ -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 (file)
index 0000000..d75a476
--- /dev/null
@@ -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 (file)
index 0000000..b84b39d
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.services.roomclimatecontrol;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.services.BoschSHCService;
+import org.openhab.binding.boschshc.internal.services.roomclimatecontrol.dto.RoomClimateControlServiceState;
+
+/**
+ * Service of a virtual device which controls the radiator thermostats in a room.
+ * 
+ * @author Christian Oeing - Initial contribution
+ */
+@NonNullByDefault
+public class RoomClimateControlService extends BoschSHCService<RoomClimateControlServiceState> {
+    public RoomClimateControlService() {
+        super("RoomClimateControl", RoomClimateControlServiceState.class);
+    }
+}
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 (file)
index 0000000..bd5a261
--- /dev/null
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.services.roomclimatecontrol.dto;
+
+import javax.measure.quantity.Temperature;
+
+import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.types.State;
+
+/**
+ * State for {@link RoomClimateControlService} to get and set the desired temperature of a room.
+ * 
+ * @author Christian Oeing - Initial contribution
+ */
+public class RoomClimateControlServiceState extends BoschSHCServiceState {
+
+    private static final String TYPE = "climateControlState";
+
+    public RoomClimateControlServiceState() {
+        super(TYPE);
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param setpointTemperature Desired temperature (in degree celsius).
+     */
+    public RoomClimateControlServiceState(double setpointTemperature) {
+        super(TYPE);
+        this.setpointTemperature = setpointTemperature;
+    }
+
+    /**
+     * Desired temperature (in degree celsius).
+     * 
+     * @apiNote Min: 5.0, Max: 30.0.
+     * @apiNote Can be set in 0.5 steps.
+     */
+    private double setpointTemperature;
+
+    /**
+     * Desired temperature state to set for a thing.
+     * 
+     * @return Desired temperature state to set for a thing.
+     */
+    public State getSetpointTemperatureState() {
+        return new QuantityType<Temperature>(this.setpointTemperature, SIUnits.CELSIUS);
+    }
+}
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 (file)
index 0000000..743c503
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.services.shuttercontact;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.services.BoschSHCService;
+import org.openhab.binding.boschshc.internal.services.shuttercontact.dto.ShutterContactServiceState;
+
+/**
+ * Service to get the state of shutters.
+ * 
+ * @author Christian Oeing - Initial contribution
+ */
+@NonNullByDefault
+public class ShutterContactService extends BoschSHCService<ShutterContactServiceState> {
+    public ShutterContactService() {
+        super("ShutterContact", ShutterContactServiceState.class);
+    }
+}
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 (file)
index 0000000..9391a56
--- /dev/null
@@ -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 (file)
index 0000000..7fccf98
--- /dev/null
@@ -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 (file)
index 0000000..0579b0e
--- /dev/null
@@ -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 (file)
index 0000000..ac7ebe3
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.services.shuttercontrol;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.services.BoschSHCService;
+import org.openhab.binding.boschshc.internal.services.shuttercontrol.dto.ShutterControlServiceState;
+
+/**
+ * Service to control the shutters of a device.
+ * 
+ * @author Christian Oeing - Initial contribution
+ */
+@NonNullByDefault
+public class ShutterControlService extends BoschSHCService<ShutterControlServiceState> {
+    public ShutterControlService() {
+        super("ShutterControl", ShutterControlServiceState.class);
+    }
+}
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 (file)
index 0000000..c7eb5db
--- /dev/null
@@ -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 (file)
index 0000000..fa66f8a
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.services.temperaturelevel;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.services.BoschSHCService;
+import org.openhab.binding.boschshc.internal.services.temperaturelevel.dto.TemperatureLevelServiceState;
+
+/**
+ * TemperatureLevel service.
+ * 
+ * @author Christian Oeing - Initial contribution
+ */
+@NonNullByDefault
+public class TemperatureLevelService extends BoschSHCService<TemperatureLevelServiceState> {
+    public TemperatureLevelService() {
+        super("TemperatureLevel", TemperatureLevelServiceState.class);
+    }
+}
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 (file)
index 0000000..4150134
--- /dev/null
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.services.temperaturelevel.dto;
+
+import javax.measure.quantity.Temperature;
+
+import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.types.State;
+
+/**
+ * TemperatureLevel service state.
+ * 
+ * @author Christian Oeing - Initial contribution
+ */
+public class TemperatureLevelServiceState extends BoschSHCServiceState {
+
+    public TemperatureLevelServiceState() {
+        super("temperatureLevelState");
+    }
+
+    /**
+     * Current temperature (in degree celsius)
+     */
+    private double temperature;
+
+    /**
+     * Current temperature state to set for a thing.
+     * 
+     * @return Current temperature state to use for a thing.
+     */
+    public State getTemperatureState() {
+        return new QuantityType<Temperature>(this.temperature, SIUnits.CELSIUS);
+    }
+}
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 (file)
index 0000000..0137e48
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.boschshc.internal.services.valvetappet;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.services.BoschSHCService;
+import org.openhab.binding.boschshc.internal.services.valvetappet.dto.ValveTappetServiceState;
+
+/**
+ * Valve Tappet service.
+ * 
+ * @author Christian Oeing - Initial contribution
+ */
+@NonNullByDefault
+public class ValveTappetService extends BoschSHCService<ValveTappetServiceState> {
+    public ValveTappetService() {
+        super("ValveTappet", ValveTappetServiceState.class);
+    }
+}
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 (file)
index 0000000..3c7e856
--- /dev/null
@@ -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 (file)
index 0000000..e7346d1
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="boschshc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+       <name>Bosch Smart Home Binding</name>
+       <description>This is the binding for Bosch Smart Home Controller.</description>
+       <author>Stefan Kästle</author>
+
+</binding:binding>
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 (file)
index 0000000..0dbe7d9
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+       <config-description uri="thing-type:boschshc:bridge">
+               <parameter name="ipAddress" type="text" required="true">
+                       <context>network-address</context>
+                       <label>Network Address</label>
+                       <description>Network address of the Bosch Smart Home Controller.</description>
+               </parameter>
+               <parameter name="password" type="text" required="true">
+                       <label>System Password</label>
+                       <context>password</context>
+                       <description>The system password of the Bosch Smart Home Controller necessary for pairing.</description>
+               </parameter>
+       </config-description>
+       <config-description uri="thing-type:boschshc:device">
+               <parameter name="id" type="text" required="true">
+                       <label>Device ID</label>
+                       <description>Unique ID of the device.</description>
+               </parameter>
+       </config-description>
+</config-description:config-descriptions>
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 (file)
index 0000000..e25fee3
--- /dev/null
@@ -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 (file)
index 0000000..0ef037c
--- /dev/null
@@ -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 (file)
index 0000000..35f4eb5
--- /dev/null
@@ -0,0 +1,262 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="boschshc"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+       <!-- Bosch Bridge -->
+       <bridge-type id="shc">
+               <label>Smart Home Controller</label>
+               <description>The Bosch SHC Bridge representing the Bosch Smart Home Controller.</description>
+
+               <config-description-ref uri="thing-type:boschshc:bridge"/>
+       </bridge-type>
+
+       <thing-type id="in-wall-switch">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="shc"/>
+               </supported-bridge-type-refs>
+
+               <label>In-wall Switch</label>
+               <description>Bosch In-wall switch for light control</description>
+
+               <channels>
+                       <channel id="power-switch" typeId="system.power"/>
+                       <channel id="power-consumption" typeId="power-consumption"/>
+                       <channel id="energy-consumption" typeId="energy-consumption"/>
+               </channels>
+
+               <config-description-ref uri="thing-type:boschshc:device"/>
+
+       </thing-type>
+
+       <thing-type id="twinguard">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="shc"/>
+               </supported-bridge-type-refs>
+
+               <label>TwinGuard</label>
+               <description>Bosch TwinGuard environmental sensor</description>
+
+               <channels>
+                       <channel id="temperature" typeId="temperature"/>
+                       <channel id="temperature-rating" typeId="temperature-rating"/>
+                       <channel id="humidity" typeId="humidity"/>
+                       <channel id="humidity-rating" typeId="humidity-rating"/>
+                       <channel id="purity" typeId="purity"/>
+                       <channel id="air-description" typeId="air-description"/>
+                       <channel id="purity-rating" typeId="purity-rating"/>
+                       <channel id="combined-rating" typeId="combined-rating"/>
+               </channels>
+
+               <config-description-ref uri="thing-type:boschshc:device"/>
+
+       </thing-type>
+
+       <thing-type id="window-contact">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="shc"/>
+               </supported-bridge-type-refs>
+
+               <label>Window/Door Contact</label>
+               <description>Bosch Contact for windows and doors</description>
+
+               <channels>
+                       <channel id="contact" typeId="contact"/>
+               </channels>
+
+               <config-description-ref uri="thing-type:boschshc:device"/>
+
+       </thing-type>
+
+       <thing-type id="motion-detector">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="shc"/>
+               </supported-bridge-type-refs>
+
+               <label>Motion Detector</label>
+               <description>Bosch Motion Detector</description>
+
+               <channels>
+                       <channel id="latest-motion" typeId="latest-motion"/>
+               </channels>
+
+               <config-description-ref uri="thing-type:boschshc:device"/>
+
+       </thing-type>
+
+       <thing-type id="shutter-control">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="shc"/>
+               </supported-bridge-type-refs>
+
+               <label>Shutter Control</label>
+               <description>Bosch Shutter Control</description>
+
+               <channels>
+                       <channel id="level" typeId="level"/>
+               </channels>
+
+               <config-description-ref uri="thing-type:boschshc:device"/>
+
+       </thing-type>
+
+       <thing-type id="thermostat">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="shc"/>
+               </supported-bridge-type-refs>
+
+               <label>Thermostat</label>
+               <description>Bosch Thermostat</description>
+
+               <channels>
+                       <channel id="temperature" typeId="temperature"/>
+                       <channel id="valve-tappet-position" typeId="valve-tappet-position"/>
+               </channels>
+
+               <config-description-ref uri="thing-type:boschshc:device"/>
+
+       </thing-type>
+
+       <thing-type id="climate-control">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="shc"/>
+               </supported-bridge-type-refs>
+
+               <label>Climate Control</label>
+               <description>Bosch Climate Control. This is a virtual device which is automatically created for all rooms that have
+                       thermostats in it.</description>
+
+               <channels>
+                       <channel id="temperature" typeId="temperature"/>
+                       <channel id="setpoint-temperature" typeId="setpoint-temperature"/>
+               </channels>
+
+               <config-description-ref uri="thing-type:boschshc:device"/>
+
+       </thing-type>
+
+       <channel-type id="temperature">
+               <item-type>Number:Temperature</item-type>
+               <label>Temperature</label>
+               <description>Current measured temperature.</description>
+               <state min="0" max="40" step="0.5" pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="temperature-rating">
+               <item-type>String</item-type>
+               <label>Temperature Rating</label>
+               <description>Rating of the currently measured temperature.</description>
+               <state readOnly="true">
+                       <options>
+                               <option value="GOOD">Good Temperature</option>
+                               <option value="MEDIUM">Medium Temperature</option>
+                               <option value="BAD">Bad Temperature</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="humidity">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Humidity</label>
+               <description>Current measured humidity.</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="humidity-rating">
+               <item-type>String</item-type>
+               <label>Humidity Rating</label>
+               <description>Rating of current measured humidity.</description>
+               <state readOnly="true">
+                       <options>
+                               <option value="GOOD">Good Humidity</option>
+                               <option value="MEDIUM">Medium Humidity</option>
+                               <option value="BAD">Bad Humidity</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="energy-consumption">
+               <item-type>Number:Energy</item-type>
+               <label>Energy consumption (Wh)</label>
+               <description>Energy consumption of the device.</description>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="power-consumption">
+               <item-type>Number:Power</item-type>
+               <label>Power consumption (W)</label>
+               <description>Current power consumption of the device.</description>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="purity">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Purity</label>
+               <description>Purity of the air. A higher value indicates a higher pollution.</description>
+               <state min="500" max="5500" pattern="%.1f ppm" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="air-description">
+               <item-type>String</item-type>
+               <label>Description</label>
+               <description>Overall description of the air quality.</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="purity-rating">
+               <item-type>String</item-type>
+               <label>Purity Rating</label>
+               <description>Rating of the air purity.</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="combined-rating">
+               <item-type>String</item-type>
+               <label>Combined Rating</label>
+               <description>Combined rating of the air quality.</description>
+               <state readOnly="true">
+                       <options>
+                               <option value="GOOD">Good Quality</option>
+                               <option value="MEDIUM">Medium Quality</option>
+                               <option value="BAD">Bad Quality</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="contact">
+               <item-type>Contact</item-type>
+               <label>Window/Door contact</label>
+               <description>A window and door contact.</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="latest-motion">
+               <item-type>DateTime</item-type>
+               <label>Latest motion</label>
+               <description>Timestamp of the latest motion.</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="level">
+               <item-type>Rollershutter</item-type>
+               <label>Level</label>
+               <description>Current open ratio (0 to 100).</description>
+               <state min="0" max="100" step="0.5" readOnly="false"/>
+       </channel-type>
+
+       <channel-type id="valve-tappet-position">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Valve Tappet Position</label>
+               <description>Current open ratio (0 to 100).</description>
+               <state min="0" max="100" step="1" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="setpoint-temperature">
+               <item-type>Number:Temperature</item-type>
+               <label>Setpoint Temperature</label>
+               <description>Desired temperature.</description>
+               <state min="5" max="30" step="0.5" pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+</thing:thing-descriptions>
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 (file)
index 0000000..25eb08c
--- /dev/null
@@ -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 (file)
index 0000000..86edc75
--- /dev/null
@@ -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 (file)
index 0000000..3bbf82a
--- /dev/null
@@ -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 (file)
index 0000000..3da600f
--- /dev/null
@@ -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 (file)
index 0000000..739ac2c
--- /dev/null
@@ -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);
+        }
+    }
+}
index 394835617a4c72a93dd183697a0a1c6c36e19fe4..fcd79be957a2f45b64e2b80ced893b13da51bee6 100644 (file)
@@ -65,6 +65,7 @@
     <module>org.openhab.binding.bluetooth.roaming</module>
     <module>org.openhab.binding.bluetooth.ruuvitag</module>
     <module>org.openhab.binding.boschindego</module>
+    <module>org.openhab.binding.boschshc</module>
     <module>org.openhab.binding.bosesoundtouch</module>
     <module>org.openhab.binding.bsblan</module>
     <module>org.openhab.binding.bticinosmarther</module>