From 2a5bdf3b47215d4c27a273d3a4db1459a174d8bf Mon Sep 17 00:00:00 2001 From: Christian Oeing Date: Sun, 17 Jan 2021 22:20:20 +0100 Subject: [PATCH] [boschshc] Initial contribution - Bindings for Bosch Smart Home devices (#8629) * Initial code from create_openhab_binding_skeleton.sh Signed-off-by: Stefan Kaestle Signed-off-by: Christian Oeing Signed-off-by: Gerd Zanker Co-authored-by: Stefan Kaestle Co-authored-by: Gerd Zanker Co-authored-by: Christian Oeing Co-authored-by: Hilbrand Bouwkamp Co-authored-by: Fabian Wolter Co-authored-by: Connor Petty --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + .../DEVELOPERS.md | 52 +++ bundles/org.openhab.binding.boschshc/NOTICE | 21 + .../org.openhab.binding.boschshc/README.md | 170 ++++++++ bundles/org.openhab.binding.boschshc/pom.xml | 32 ++ .../src/main/feature/feature.xml | 9 + .../devices/BoschSHCBindingConstants.java | 59 +++ .../devices/BoschSHCConfiguration.java | 29 ++ .../internal/devices/BoschSHCHandler.java | 299 +++++++++++++ .../devices/BoschSHCHandlerFactory.java | 91 ++++ .../devices/bridge/BoschHttpClient.java | 247 +++++++++++ .../bridge/BoschSHCBridgeConfiguration.java | 34 ++ .../devices/bridge/BoschSHCBridgeHandler.java | 410 ++++++++++++++++++ .../internal/devices/bridge/BoschSslUtil.java | 219 ++++++++++ .../devices/bridge/JsonRpcRequest.java | 62 +++ .../internal/devices/bridge/LongPolling.java | 211 +++++++++ .../internal/devices/bridge/dto/Device.java | 57 +++ .../bridge/dto/DeviceStatusUpdate.java | 56 +++ .../devices/bridge/dto/LongPollError.java | 41 ++ .../devices/bridge/dto/LongPollResult.java | 40 ++ .../internal/devices/bridge/dto/Room.java | 32 ++ .../devices/bridge/dto/SubscribeResult.java | 33 ++ .../climatecontrol/ClimateControlHandler.java | 106 +++++ .../BoschInWallSwitchHandler.java | 133 ++++++ .../inwallswitch/dto/PowerMeterState.java | 30 ++ .../motiondetector/MotionDetectorHandler.java | 72 +++ .../motiondetector/dto/LatestMotionState.java | 43 ++ .../shuttercontrol/ShutterControlHandler.java | 106 +++++ .../devices/thermostat/ThermostatHandler.java | 64 +++ .../twinguard/BoschTwinguardHandler.java | 92 ++++ .../twinguard/dto/AirQualityLevelState.java | 61 +++ .../windowcontact/WindowContactHandler.java | 50 +++ .../exceptions/BoschSHCException.java | 35 ++ .../LongPollingFailedException.java | 28 ++ .../exceptions/PairingFailedException.java | 35 ++ .../internal/services/BoschSHCService.java | 198 +++++++++ .../services/dto/BoschSHCServiceState.java | 33 ++ .../dto/JsonRestExceptionResponse.java | 36 ++ .../powerswitch/PowerSwitchService.java | 30 ++ .../powerswitch/PowerSwitchState.java | 26 ++ .../dto/PowerSwitchServiceState.java | 34 ++ .../RoomClimateControlService.java | 29 ++ .../dto/RoomClimateControlServiceState.java | 61 +++ .../shuttercontact/ShutterContactService.java | 29 ++ .../shuttercontact/ShutterContactState.java | 26 ++ .../dto/ShutterContactServiceState.java | 32 ++ .../shuttercontrol/OperationState.java | 26 ++ .../shuttercontrol/ShutterControlService.java | 29 ++ .../dto/ShutterControlServiceState.java | 43 ++ .../TemperatureLevelService.java | 29 ++ .../dto/TemperatureLevelServiceState.java | 46 ++ .../valvetappet/ValveTappetService.java | 29 ++ .../dto/ValveTappetServiceState.java | 40 ++ .../main/resources/OH-INF/binding/binding.xml | 10 + .../main/resources/OH-INF/config/configs.xml | 24 + .../resources/OH-INF/i18n/boschshc.properties | 5 + .../OH-INF/i18n/boschshc_de.properties | 9 + .../resources/OH-INF/thing/thing-types.xml | 262 +++++++++++ .../bridge/SmartHomeControllerIssuingCA.pem | 30 ++ .../SmartHomeControllerProductiveRootCA.pem | 33 ++ .../devices/bridge/BoschHttpClientTest.java | 103 +++++ .../devices/bridge/BoschSslUtilTest.java | 102 +++++ .../bridge/dto/LongPollResultTest.java | 42 ++ bundles/pom.xml | 1 + 65 files changed, 4462 insertions(+) create mode 100644 bundles/org.openhab.binding.boschshc/DEVELOPERS.md create mode 100644 bundles/org.openhab.binding.boschshc/NOTICE create mode 100644 bundles/org.openhab.binding.boschshc/README.md create mode 100644 bundles/org.openhab.binding.boschshc/pom.xml create mode 100644 bundles/org.openhab.binding.boschshc/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCConfiguration.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeConfiguration.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeHandler.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSslUtil.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/JsonRpcRequest.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Device.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/DeviceStatusUpdate.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollError.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResult.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Room.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/SubscribeResult.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/climatecontrol/ClimateControlHandler.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/inwallswitch/BoschInWallSwitchHandler.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/inwallswitch/dto/PowerMeterState.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/MotionDetectorHandler.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/dto/LatestMotionState.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/shuttercontrol/ShutterControlHandler.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandler.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/BoschTwinguardHandler.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/dto/AirQualityLevelState.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/windowcontact/WindowContactHandler.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/BoschSHCException.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/LongPollingFailedException.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/PairingFailedException.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/BoschSHCService.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceState.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/JsonRestExceptionResponse.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchService.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchState.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/dto/PowerSwitchServiceState.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/roomclimatecontrol/RoomClimateControlService.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/roomclimatecontrol/dto/RoomClimateControlServiceState.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/ShutterContactService.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/ShutterContactState.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/dto/ShutterContactServiceState.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/OperationState.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/ShutterControlService.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/dto/ShutterControlServiceState.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperaturelevel/TemperatureLevelService.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperaturelevel/dto/TemperatureLevelServiceState.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/valvetappet/ValveTappetService.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/valvetappet/dto/ValveTappetServiceState.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/config/configs.xml create mode 100644 bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties create mode 100644 bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc_de.properties create mode 100644 bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.boschshc/src/main/resources/org/openhab/binding/boschshc/internal/devices/bridge/SmartHomeControllerIssuingCA.pem create mode 100644 bundles/org.openhab.binding.boschshc/src/main/resources/org/openhab/binding/boschshc/internal/devices/bridge/SmartHomeControllerProductiveRootCA.pem create mode 100644 bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java create mode 100644 bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSslUtilTest.java create mode 100644 bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResultTest.java diff --git a/CODEOWNERS b/CODEOWNERS index 363278450ac17..870d7f28d76cf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -34,6 +34,7 @@ /bundles/org.openhab.binding.bluetooth.roaming/ @cpmeister /bundles/org.openhab.binding.bluetooth.ruuvitag/ @ssalonen /bundles/org.openhab.binding.boschindego/ @jofleck +/bundles/org.openhab.binding.boschshc/ @stefan-kaestle @coeing @GerdZanker /bundles/org.openhab.binding.bosesoundtouch/ @marvkis @tratho /bundles/org.openhab.binding.bsblan/ @hypetsch /bundles/org.openhab.binding.bticinosmarther/ @MrRonfo diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index b3039866c6cff..c15457a389fad 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -156,6 +156,11 @@ org.openhab.binding.boschindego ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.boschshc + ${project.version} + org.openhab.addons.bundles org.openhab.binding.bosesoundtouch diff --git a/bundles/org.openhab.binding.boschshc/DEVELOPERS.md b/bundles/org.openhab.binding.boschshc/DEVELOPERS.md new file mode 100644 index 0000000000000..9ee8b4bb7e40e --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/DEVELOPERS.md @@ -0,0 +1,52 @@ +# For Developers + +## Build + +To only build the Bosch SHC binding code execute + + mvn -pl :org.openhab.binding.boschshc install + +## Execute + +After compiling a new ``org.openhab.binding.boschshc.jar`` +copy it into the ``addons`` folder of your openHAB test instance. + +For the first time the jar is loaded automatically as a bundle. + +It should also be reloaded automatically when the jar changed. + +To reload the bundle manually you need to execute: + + bundle:update "openHAB Add-ons :: Bundles :: BoschSHC Binding" + +or get the ID and update the bundle using the ID: + + bundle:list + -> Get ID for "openHAB Add-ons :: Bundles :: BoschSHC Binding" + bundle:update + + +## Debugging + +To get debug output and traces of the Bosch SHC binding code +add the following lines into ``userdata/etc/log4j2.xml`` Loggers XML section. + + + + +## Pairing and Certificates + +We need secured and paired connection from the openHAB binding instance to the Bosch SHC. + +Read more about the pairing process in [register a new client to the bosch smart home controller](/~https://github.com/BoschSmartHome/bosch-shc-api-docs/tree/master/postman#register-a-new-client-to-the-bosch-smart-home-controller) + +A precondition for the secured connection to the Bosch SHC is a self singed key + certificate. +The key + certificate will be created and stored with the public Bosch SHC certificates in a Java Key store (jks). + +The public certificates files are from /~https://github.com/BoschSmartHome/bosch-shc-api-docs/tree/master/best_practice. +File copies stored in ``src/main/resource``. + +All three certificates and the key will be used for the HTTPS connection between +this openHAB binding and the Bosch SHC. + +During pairing the openHAB binding will exchange the self singed certificate with SHC. \ No newline at end of file diff --git a/bundles/org.openhab.binding.boschshc/NOTICE b/bundles/org.openhab.binding.boschshc/NOTICE new file mode 100644 index 0000000000000..bc35a20550963 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/NOTICE @@ -0,0 +1,21 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +/~https://github.com/openhab/openhab-addons + +## Third-party Content + +bcpkix-jdk15on +bcprov-jdk15on +* License: Bouncy Castle License +* Project: https://www.bouncycastle.org +* Source: /~https://github.com/bcgit/bc-java \ No newline at end of file diff --git a/bundles/org.openhab.binding.boschshc/README.md b/bundles/org.openhab.binding.boschshc/README.md new file mode 100644 index 0000000000000..b55abc659bfbd --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/README.md @@ -0,0 +1,170 @@ +# BoschSHC Binding + +Binding for the Bosch Smart Home Controller. + +- [BoschSHC Binding](#boschshc-binding) + - [Supported Things](#supported-things) + - [Bosch In-Wall switches & Bosch Smart Plugs](#bosch-in-wall-switches--bosch-smart-plugs) + - [Bosch TwinGuard smoke detector](#bosch-twinguard-smoke-detector) + - [Bosch Window/Door contacts](#bosch-windowdoor-contacts) + - [Bosch Motion Detector](#bosch-motion-detector) + - [Bosch Shutter Control in-wall](#bosch-shutter-control-in-wall) + - [Bosch Thermostat](#bosch-thermostat) + - [Bosch Climate Control](#bosch-climate-control) + - [Limitations](#limitations) + - [Discovery](#discovery) + - [Binding Configuration](#binding-configuration) + - [Getting the device IDs](#getting-the-device-ids) + - [Thing Configuration](#thing-configuration) + - [Item Configuration](#item-configuration) + +## Supported Things + +### Bosch In-Wall switches & Bosch Smart Plugs + +**Thing Type ID**: `in-wall-switch` + +| Channel Type ID | Item Type | Description | +|--------------------|---------------|----------------------------------------------| +| power-switch | Switch | Current state of the switch. | +| power-consumption | Number:Power | Current power consumption (W) of the device. | +| energy-consumption | Number:Energy | Energy consumption of the device. | + +### Bosch TwinGuard smoke detector + +**Thing Type ID**: `twinguard` + +| Channel Type ID | Item Type | Description | +|--------------------|----------------------|---------------------------------------------------------------------------------------------------| +| temperature | Number:Temperature | Current measured temperature. | +| temperature-rating | String | Rating of the currently measured temperature. | +| humidity | Number:Dimensionless | Current measured humidity. | +| humidity-rating | String | Rating of current measured humidity. | +| purity | Number:Dimensionless | Purity of the air (ppm). Range from 500 to 5500 ppm. A higher value indicates a higher pollution. | +| purity-rating | String | Rating of current measured purity. | +| air-description | String | Overall description of the air quality. | +| combined-rating | String | Combined rating of the air quality. | + +### Bosch Window/Door contacts + +**Thing Type ID**: `window-contact` + +| Channel Type ID | Item Type | Description | +|-----------------|-----------|------------------------------| +| contact | Contact | Contact state of the device. | + +### Bosch Motion Detector + +**Thing Type ID**: `motion-detector` + +| Channel Type ID | Item Type | Description | +|-----------------|-----------|--------------------------------| +| latest-motion | DateTime | The date of the latest motion. | + +### Bosch Shutter Control in-wall + +**Thing Type ID**: `shutter-control` + +| Channel Type ID | Item Type | Description | +|-----------------|---------------|------------------------------------------| +| level | Rollershutter | Current open ratio (0 to 100, Step 0.5). | + +### Bosch Thermostat + +**Thing Type ID**: `thermostat` + +| Channel Type ID | Item Type | Description | +|-----------------------|----------------------|------------------------------------------------| +| temperature | Number:Temperature | Current measured temperature. | +| valve-tappet-position | Number:Dimensionless | Current open ratio of valve tappet (0 to 100). | + +### Bosch Climate Control + +**Thing Type ID**: `climate-control` + +| Channel Type ID | Item Type | Description | +|----------------------|--------------------|-------------------------------| +| temperature | Number:Temperature | Current measured temperature. | +| setpoint-temperature | Number:Temperature | Desired temperature. | + +## Limitations + +- Discovery of Things +- Discovery of Bridge + +## Discovery + +Configuration via configuration files or UI (see below). + +## Bridge Configuration + +You need to provide the IP address and the system password of your Bosch Smart Home Controller. +The IP address of the controller is visible in the Bosch Smart Home Mobile App (More -> System -> Smart Home Controller) or in your network router UI. +The system password is set by you during your initial registration steps in the _Bosch Smart Home App_. + +A keystore file with a self signed certificate is created automatically. +This certificate is used for pairing between the Bridge and the Bosch SHC. + +*Press and hold the Bosch Smart Home Controller Bridge button until the LED starts blinking after you save your settings for pairing*. + +## Getting the device IDs + +Bosch IDs for found devices are displayed in the openHAB log on bootup (`OPENHAB_FOLDER/userdata/logs/openhab.log`) + +Example: + +``` +2020-08-11 12:42:49.490 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Heizung id=hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX +2020-08-11 12:42:49.495 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=-RoomClimateControl- id=roomClimateControl_hz_1 +2020-08-11 12:42:49.497 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=-VentilationService- id=ventilationService +2020-08-11 12:42:49.498 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Großes Fenster id=hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX +2020-08-11 12:42:49.501 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=-IntrusionDetectionSystem- id=intrusionDetectionSystem +2020-08-11 12:42:49.502 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Rollladen id=hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX +2020-08-11 12:42:49.502 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Heizung id=hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX +2020-08-11 12:42:49.503 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Heizung Haus id=hdm:ICom:819410185:HC1 +2020-08-11 12:42:49.503 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=-RoomClimateControl- id=roomClimateControl_hz_6 +2020-08-11 12:42:49.504 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=PhilipsHueBridgeManager id=hdm:PhilipsHueBridge:PhilipsHueBridgeManager +2020-08-11 12:42:49.505 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Rollladen id=hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX +2020-08-11 12:42:49.506 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Rollladen id=hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX +2020-08-11 12:42:49.507 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Central Heating id=hdm:ICom:819410185 +``` + +## Thing Configuration + +You define your Bosch devices by adding them either to a `.things` file in your `$OPENHAB_CONF/things` folder like this: + +``` +Bridge boschshc:shc:1 [ ipAddress="192.168.x.y", password="XXXXXXXXXX" ] { + Thing in-wall-switch bathroom "Bathroom" [ id="hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX" ] + Thing in-wall-switch bedroom "Bedroom" [ id="hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX" ] + Thing in-wall-switch kitchen "Kitchen" [ id="hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX" ] + Thing in-wall-switch corridor "Corridor" [ id="hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX" ] + Thing in-wall-switch livingroom "Living Room" [ id="hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX" ] + + Thing in-wall-switch coffeemachine "Coffee Machine" [ id="hdm:HomeMaticIP:3014F711A0000XXXXXXXXXXXX" ] + + Thing twinguard tg-corridor "Twinguard Smoke Detector" [ id="hdm:ZigBee:000d6f000XXXXXXX" ] + Thing window-contact window-kitchen "Window Kitchen" [ id="hdm:HomeMaticIP:3014F711A00000XXXXXXXXXX" ] + Thing window-contact entrance "Entrance door" [ id="hdm:HomeMaticIP:3014F711A00000XXXXXXXXXX" ] + + Thing motion-detector motion-corridor "Bewegungsmelder" [ id="hdm:ZigBee:000d6f000XXXXXXX" ] +} +``` + +Or by adding them via UI: Settings -> Things -> "+" -> Bosch Smart Home Binding. + +## Item Configuration + +You define the items which should be linked to your Bosch devices via a `.items` file in your `$OPENHAB_CONF/items` folder like this: + +``` +Switch Bosch_Bathroom "Bath Room" { channel="boschshc:in-wall-switch:1:bathroom:power-switch" } +Switch Bosch_Bedroom "Bed Room" { channel="boschshc:in-wall-switch:1:bedroom:power-switch" } +Switch Bosch_Kitchen "Kitchen" { channel="boschshc:in-wall-switch:1:kitchen:power-switch" } +Switch Bosch_Corridor "Corridor" { channel="boschshc:in-wall-switch:1:corridor:power-switch" } +Switch Bosch_Living_Room "Living Room" { channel="boschshc:in-wall-switch:1:livingroom:power-switch" } + +Switch Bosch_Lelit "Lelit" { channel="boschshc:in-wall-switch:1:coffeemachine:power-switch" } +``` + +Or by adding them via UI: Settings -> Items -> "+". diff --git a/bundles/org.openhab.binding.boschshc/pom.xml b/bundles/org.openhab.binding.boschshc/pom.xml new file mode 100644 index 0000000000000..b85d42a4594b1 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/pom.xml @@ -0,0 +1,32 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.binding.boschshc + + openHAB Add-ons :: Bundles :: BoschSHC Binding + + + + org.bouncycastle + bcpkix-jdk15on + 1.52 + compile + + + org.bouncycastle + bcprov-jdk15on + 1.52 + compile + + + + diff --git a/bundles/org.openhab.binding.boschshc/src/main/feature/feature.xml b/bundles/org.openhab.binding.boschshc/src/main/feature/feature.xml new file mode 100644 index 0000000000000..314d44d31a791 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.boschshc/${project.version} + + diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java new file mode 100644 index 0000000000000..47e85f9fcf46c --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link BoschSHCBindingConstants} class defines common constants, which + * are used across the whole binding. + * + * @author Stefan Kästle - Initial contribution + * @author Christian Oeing - added Shutter Control, ThermostatHandler + */ +@NonNullByDefault +public class BoschSHCBindingConstants { + + private static final String BINDING_ID = "boschshc"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_SHC = new ThingTypeUID(BINDING_ID, "shc"); + + public static final ThingTypeUID THING_TYPE_INWALL_SWITCH = new ThingTypeUID(BINDING_ID, "in-wall-switch"); + public static final ThingTypeUID THING_TYPE_TWINGUARD = new ThingTypeUID(BINDING_ID, "twinguard"); + public static final ThingTypeUID THING_TYPE_WINDOW_CONTACT = new ThingTypeUID(BINDING_ID, "window-contact"); + public static final ThingTypeUID THING_TYPE_MOTION_DETECTOR = new ThingTypeUID(BINDING_ID, "motion-detector"); + public static final ThingTypeUID THING_TYPE_SHUTTER_CONTROL = new ThingTypeUID(BINDING_ID, "shutter-control"); + public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat"); + public static final ThingTypeUID THING_TYPE_CLIMATE_CONTROL = new ThingTypeUID(BINDING_ID, "climate-control"); + + // List of all Channel IDs + // Auto-generated from thing-types.xml via script, don't modify + public static final String CHANNEL_POWER_SWITCH = "power-switch"; + public static final String CHANNEL_TEMPERATURE = "temperature"; + public static final String CHANNEL_TEMPERATURE_RATING = "temperature-rating"; + public static final String CHANNEL_HUMIDITY = "humidity"; + public static final String CHANNEL_HUMIDITY_RATING = "humidity-rating"; + public static final String CHANNEL_ENERGY_CONSUMPTION = "energy-consumption"; + public static final String CHANNEL_POWER_CONSUMPTION = "power-consumption"; + public static final String CHANNEL_PURITY = "purity"; + public static final String CHANNEL_AIR_DESCRIPTION = "air-description"; + public static final String CHANNEL_PURITY_RATING = "purity-rating"; + public static final String CHANNEL_COMBINED_RATING = "combined-rating"; + public static final String CHANNEL_CONTACT = "contact"; + public static final String CHANNEL_LATEST_MOTION = "latest-motion"; + public static final String CHANNEL_LEVEL = "level"; + public static final String CHANNEL_VALVE_TAPPET_POSITION = "valve-tappet-position"; + public static final String CHANNEL_SETPOINT_TEMPERATURE = "setpoint-temperature"; +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCConfiguration.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCConfiguration.java new file mode 100644 index 0000000000000..f1ed032b6912c --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCConfiguration.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link BoschSHCConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Stefan Kästle - Initial contribution + */ +@NonNullByDefault +public class BoschSHCConfiguration { + /** + * ID of the device as returned by the controller. + */ + public @Nullable String id; +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java new file mode 100644 index 0000000000000..a683a1f5da40a --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java @@ -0,0 +1,299 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.boschshc.internal.devices.bridge.BoschSHCBridgeHandler; +import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; +import org.openhab.binding.boschshc.internal.services.BoschSHCService; +import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; + +/** + * The {@link BoschSHCHandler} represents Bosch Things. Each type of device + * inherits from this abstract thing handler. + * + * @author Stefan Kästle - Initial contribution + * @author Christian Oeing - refactorings of e.g. server registration + */ +@NonNullByDefault +public abstract class BoschSHCHandler extends BaseThingHandler { + + /** + * Service State for a Bosch device. + */ + class DeviceService { + /** + * Constructor. + * + * @param service Service which belongs to the device. + * @param affectedChannels Channels which are affected by the state of this service. + */ + public DeviceService(BoschSHCService service, Collection affectedChannels) { + this.service = service; + this.affectedChannels = affectedChannels; + } + + /** + * Service which belongs to the device. + */ + public final BoschSHCService service; + + /** + * Channels which are affected by the state of this service. + */ + public final Collection affectedChannels; + } + + /** + * Reusable gson instance to convert a class to json string and back in derived classes. + */ + protected static final Gson GSON = new Gson(); + + protected final Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * Bosch SHC configuration loaded from openHAB configuration. + */ + private @Nullable BoschSHCConfiguration config; + + /** + * Services of the device. + */ + private List> services = new ArrayList<>(); + + public BoschSHCHandler(Thing thing) { + super(thing); + } + + /** + * Returns the unique id of the Bosch device. + * + * @return Unique id of the Bosch device. + */ + public @Nullable String getBoschID() { + BoschSHCConfiguration config = this.config; + if (config != null) { + return config.id; + } else { + return null; + } + } + + /** + * Initializes this handler. Use this method to register all services of the device with + * {@link #registerService(BoschSHCService)}. + */ + @Override + public void initialize() { + this.config = getConfigAs(BoschSHCConfiguration.class); + + try { + this.initializeServices(); + + // Mark immediately as online - if the bridge is online, the thing is too. + this.updateStatus(ThingStatus.ONLINE); + } catch (BoschSHCException e) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + } + } + + /** + * Handles the refresh command of all registered services. Override it to handle custom commands (e.g. to update + * states of services). + * + * @param channelUID {@link ChannelUID} of the channel to which the command was sent + * @param command {@link Command} + */ + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + // Refresh state of services that affect the channel + for (DeviceService 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 deviceService : this.services) { + BoschSHCService service = deviceService.service; + if (serviceName.equals(service.getServiceName())) { + service.onStateUpdate(stateData); + } + } + } + + /** + * Should be used by handlers to create their required services. + */ + protected void initializeServices() throws BoschSHCException { + } + + /** + * Returns the bridge handler for this thing handler. + * + * @return Bridge handler for this thing handler. Null if no or an invalid bridge was set in the configuration. + * @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set. + */ + protected BoschSHCBridgeHandler getBridgeHandler() throws BoschSHCException { + Bridge bridge = this.getBridge(); + if (bridge == null) { + throw new BoschSHCException(String.format("No valid bridge set for %s", this.getThing())); + } + BoschSHCBridgeHandler bridgeHandler = (BoschSHCBridgeHandler) bridge.getHandler(); + if (bridgeHandler == null) { + throw new BoschSHCException(String.format("Bridge of %s has no valid bridge handler", this.getThing())); + } + return bridgeHandler; + } + + /** + * Query the Bosch Smart Home Controller for the state of the service with the specified name. + * + * @note Use services instead of directly requesting a state. + * + * @param stateName Name of the service to query + * @param classOfT Class to convert the resulting JSON to + */ + protected @Nullable T getState(String stateName, Class classOfT) { + String deviceId = this.getBoschID(); + if (deviceId == null) { + return null; + } + try { + BoschSHCBridgeHandler bridgeHandler = this.getBridgeHandler(); + return bridgeHandler.getState(deviceId, stateName, classOfT); + } catch (InterruptedException | TimeoutException | ExecutionException | BoschSHCException e) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + String.format("Error when trying to refresh state from service %s: %s", stateName, e.getMessage())); + return null; + } + } + + /** + * Creates and registers a new service for this device. + * + * @param Type of service. + * @param Type of service state. + * @param newService Supplier function to create a new instance of the service. + * @param stateUpdateListener Function to call when a state update was received + * from the device. + * @param affectedChannels Channels which are affected by the state of this + * service. + * @return Instance of registered service. + * @throws BoschSHCException + */ + protected , TState extends BoschSHCServiceState> TService createService( + Supplier newService, Consumer stateUpdateListener, Collection affectedChannels) + throws BoschSHCException { + TService service = newService.get(); + this.registerService(service, stateUpdateListener, affectedChannels); + return service; + } + + /** + * Registers a service for this device. + * + * @param Type of service. + * @param Type of service state. + * @param service Service to register. + * @param stateUpdateListener Function to call when a state update was received + * from the device. + * @param affectedChannels Channels which are affected by the state of this + * service. + * @throws BoschSHCException If bridge for handler is not set or an invalid bridge is set. + * @throws BoschSHCException If no device id is set. + */ + protected , TState extends BoschSHCServiceState> void registerService( + TService service, Consumer stateUpdateListener, Collection affectedChannels) + throws BoschSHCException { + BoschSHCBridgeHandler bridgeHandler = this.getBridgeHandler(); + + String deviceId = this.getBoschID(); + if (deviceId == null) { + throw new BoschSHCException( + String.format("Could not register service for %s, no device id set", this.getThing())); + } + + service.initialize(bridgeHandler, deviceId, stateUpdateListener); + this.registerService(service, affectedChannels); + } + + /** + * Updates the state of a device service. + * Sets the status of the device to offline if setting the state fails. + * + * @param Type of service. + * @param Type of service state. + * @param service Service to set state for. + * @param state State to set. + */ + protected , TState extends BoschSHCServiceState> void updateServiceState( + TService service, TState state) { + try { + service.setState(state); + } catch (InterruptedException | TimeoutException | ExecutionException e) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String.format( + "Error when trying to update state for service %s: %s", service.getServiceName(), e.getMessage())); + } + } + + /** + * Registers a service of this device. + * + * @param service Service which belongs to this device + * @param affectedChannels Channels which are affected by the state of this + * service + */ + private void registerService(BoschSHCService service, + Collection affectedChannels) { + this.services.add(new DeviceService(service, affectedChannels)); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java new file mode 100644 index 0000000000000..19a1e94198250 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices; + +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_CLIMATE_CONTROL; +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH; +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_MOTION_DETECTOR; +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SHC; +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL; +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_THERMOSTAT; +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_TWINGUARD; +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT; + +import java.util.Collection; +import java.util.List; +import java.util.function.Function; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.boschshc.internal.devices.bridge.BoschSHCBridgeHandler; +import org.openhab.binding.boschshc.internal.devices.climatecontrol.ClimateControlHandler; +import org.openhab.binding.boschshc.internal.devices.inwallswitch.BoschInWallSwitchHandler; +import org.openhab.binding.boschshc.internal.devices.motiondetector.MotionDetectorHandler; +import org.openhab.binding.boschshc.internal.devices.shuttercontrol.ShutterControlHandler; +import org.openhab.binding.boschshc.internal.devices.thermostat.ThermostatHandler; +import org.openhab.binding.boschshc.internal.devices.twinguard.BoschTwinguardHandler; +import org.openhab.binding.boschshc.internal.devices.windowcontact.WindowContactHandler; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Component; + +/** + * The {@link BoschSHCHandlerFactory} is responsible for creating things and + * thing handlers. + * + * @author Stefan Kästle - Initial contribution + * @author Christian Oeing - Added Shutter Control and ThermostatHandler; refactored handler mapping + */ +@NonNullByDefault +@Component(configurationPid = "binding.boschshc", service = ThingHandlerFactory.class) +public class BoschSHCHandlerFactory extends BaseThingHandlerFactory { + + private static class ThingTypeHandlerMapping { + public ThingTypeUID thingTypeUID; + public Function handlerSupplier; + + public ThingTypeHandlerMapping(ThingTypeUID thingTypeUID, Function handlerSupplier) { + this.thingTypeUID = thingTypeUID; + this.handlerSupplier = handlerSupplier; + } + } + + private static final Collection SUPPORTED_THING_TYPES = List.of( + new ThingTypeHandlerMapping(THING_TYPE_SHC, thing -> new BoschSHCBridgeHandler((Bridge) thing)), + new ThingTypeHandlerMapping(THING_TYPE_INWALL_SWITCH, BoschInWallSwitchHandler::new), + new ThingTypeHandlerMapping(THING_TYPE_TWINGUARD, BoschTwinguardHandler::new), + new ThingTypeHandlerMapping(THING_TYPE_WINDOW_CONTACT, WindowContactHandler::new), + new ThingTypeHandlerMapping(THING_TYPE_MOTION_DETECTOR, MotionDetectorHandler::new), + new ThingTypeHandlerMapping(THING_TYPE_SHUTTER_CONTROL, ShutterControlHandler::new), + new ThingTypeHandlerMapping(THING_TYPE_THERMOSTAT, ThermostatHandler::new), + new ThingTypeHandlerMapping(THING_TYPE_CLIMATE_CONTROL, ClimateControlHandler::new)); + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES.stream().anyMatch(mapping -> mapping.thingTypeUID.equals(thingTypeUID)); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + // Search for mapping for thing type and return handler for it if found. Otherwise return null. + return SUPPORTED_THING_TYPES.stream().filter(mapping -> mapping.thingTypeUID.equals(thingTypeUID)).findFirst() + .<@Nullable BaseThingHandler> map(mapping -> mapping.handlerSupplier.apply(thing)).orElse(null); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java new file mode 100644 index 0000000000000..e5609117ed0af --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java @@ -0,0 +1,247 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.bridge; + +import static org.eclipse.jetty.http.HttpMethod.GET; + +import java.nio.charset.StandardCharsets; +import java.security.KeyStoreException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; + +/** + * HTTP client using own context with private & Bosch Certs + * to pair and connect to the Bosch Smart Home Controller. + * + * @author Gerd Zanker - Initial contribution + */ +@NonNullByDefault +public class BoschHttpClient extends HttpClient { + private static final Gson GSON = new Gson(); + + private final Logger logger = LoggerFactory.getLogger(BoschHttpClient.class); + + private final String ipAddress; + private final String systemPassword; + + public BoschHttpClient(String ipAddress, String systemPassword, SslContextFactory sslContextFactory) { + super(sslContextFactory); + this.ipAddress = ipAddress; + this.systemPassword = systemPassword; + } + + /** + * Returns the pairing URL for the Bosch SHC clients, using port 8443. + * See /~https://github.com/BoschSmartHome/bosch-shc-api-docs/blob/master/postman/README.md + * + * @return URL for pairing + */ + public String getPairingUrl() { + return String.format("https://%s:8443/smarthome/clients", this.ipAddress); + } + + /** + * Returns a Bosch SHC URL for the endpoint, using port 8444. + * + * @param endpoint a endpoint, see https://apidocs.bosch-smarthome.com/local/index.html + * @return Bosch SHC URL for passed endpoint + */ + public String getBoschShcUrl(String endpoint) { + return String.format("https://%s:8444/%s", this.ipAddress, endpoint); + } + + /** + * Returns a SmartHome URL for the endpoint - shortcut of {@link BoschSslUtil::getBoschShcUrl()} + * + * @param endpoint a endpoint, see https://apidocs.bosch-smarthome.com/local/index.html + * @return SmartHome URL for passed endpoint + */ + public String getBoschSmartHomeUrl(String endpoint) { + return this.getBoschShcUrl(String.format("smarthome/%s", endpoint)); + } + + /** + * Returns a device & service URL. + * see https://apidocs.bosch-smarthome.com/local/index.html + * + * @param serviceName the name of the service + * @param deviceId the device identifier + * @return SmartHome URL for passed endpoint + */ + public String getServiceUrl(String serviceName, String deviceId) { + return this.getBoschSmartHomeUrl(String.format("devices/%s/services/%s/state", deviceId, serviceName)); + } + + /** + * Checks if the Bosch SHC can be accessed. + * + * @return true if HTTP access was successful + * @throws InterruptedException in case of an interrupt + */ + public boolean isAccessPossible() throws InterruptedException { + try { + String url = this.getBoschSmartHomeUrl("devices"); + Request request = this.createRequest(url, GET); + ContentResponse contentResponse = request.send(); + String content = contentResponse.getContentAsString(); + logger.debug("Access check response complete: {} - return code: {}", content, contentResponse.getStatus()); + return true; + } catch (TimeoutException | ExecutionException | NullPointerException e) { + logger.debug("Access check response failed because of {}!", e.getMessage()); + return false; + } + } + + /** + * Pairs this client with the Bosch SHC. + * Press pairing button on the Bosch Smart Home Controller! + * + * @return true if pairing was successful, otherwise false + * @throws InterruptedException in case of an interrupt + */ + public boolean doPairing() throws InterruptedException { + logger.trace("Starting pairing openHAB Client with Bosch SmartHomeController!"); + logger.trace("Please press the Bosch SHC button until LED starts blinking"); + + ContentResponse contentResponse; + try { + String publicCert = getCertFromSslContextFactory(); + logger.trace("Pairing with SHC {}", ipAddress); + + // JSON Rest content + Map items = new HashMap<>(); + items.put("@type", "client"); + items.put("id", BoschSslUtil.getBoschShcClientId()); // Client Id contains the unique OpenHab instance Id + items.put("name", "oss_OpenHAB_Binding"); // Client name according to + // /~https://github.com/BoschSmartHome/bosch-shc-api-docs#terms-and-conditions + items.put("primaryRole", "ROLE_RESTRICTED_CLIENT"); + items.put("certificate", "-----BEGIN CERTIFICATE-----\r" + publicCert + "\r-----END CERTIFICATE-----"); + + String url = this.getPairingUrl(); + Request request = this.createRequest(url, HttpMethod.POST, items).header("Systempassword", + Base64.getEncoder().encodeToString(this.systemPassword.getBytes(StandardCharsets.UTF_8))); + + contentResponse = request.send(); + + logger.trace("Pairing response complete: {} - return code: {}", contentResponse.getContentAsString(), + contentResponse.getStatus()); + if (201 == contentResponse.getStatus()) { + logger.debug("Pairing successful."); + return true; + } else { + logger.info("Pairing failed with response status {}.", contentResponse.getStatus()); + return false; + } + } catch (TimeoutException | CertificateEncodingException | KeyStoreException | NullPointerException e) { + logger.warn("Pairing failed with exception {}", e.getMessage()); + return false; + } catch (ExecutionException e) { + // javax.net.ssl.SSLHandshakeException: General SSLEngine problem + // => usually the pairing failed, because hardware button was not pressed. + logger.trace("Pairing failed - Details: {}", e.getMessage()); + logger.warn("Pairing failed. Was the Bosch SHC button pressed?"); + return false; + } + } + + /** + * Creates a HTTP request. + * + * @param url for the HTTP request + * @param method for the HTTP request + * @return created HTTP request instance + */ + public Request createRequest(String url, HttpMethod method) { + return this.createRequest(url, method, null); + } + + /** + * Creates a HTTP request. + * + * @param url for the HTTP request + * @param method for the HTTP request + * @param content for the HTTP request + * @return created HTTP request instance + */ + public Request createRequest(String url, HttpMethod method, @Nullable Object content) { + Request request = this.newRequest(url).method(method).header("Content-Type", "application/json"); + if (content != null) { + String body = GSON.toJson(content); + logger.trace("create request for {} and content {}", url, body); + request = request.content(new StringContentProvider(body)); + } else { + logger.trace("create request for {}", url); + } + + // Set default timeout + request.timeout(10, TimeUnit.SECONDS); + + return request; + } + + /** + * Sends a request and expects a response of the specified type. + * + * @param request Request to send + * @param responseContentClass Type of expected response + * @throws ExecutionException in case of invalid HTTP request result + * @throws TimeoutException in case of an HTTP request timeout + * @throws InterruptedException in case of an interrupt + */ + public TContent sendRequest(Request request, Class responseContentClass) + throws InterruptedException, TimeoutException, ExecutionException { + ContentResponse contentResponse = request.send(); + + logger.debug("BoschHttpClient: response complete: {} - return code: {}", contentResponse.getContentAsString(), + contentResponse.getStatus()); + + try { + @Nullable + TContent content = GSON.fromJson(contentResponse.getContentAsString(), responseContentClass); + if (content == null) { + throw new ExecutionException(String.format("Received no content in response, expected type %s", + responseContentClass.getName()), null); + } + return content; + } catch (JsonSyntaxException e) { + throw new ExecutionException(String.format("Received invalid content in response, expected type %s: %s", + responseContentClass.getName(), e.getMessage()), e); + } + } + + private String getCertFromSslContextFactory() throws KeyStoreException, CertificateEncodingException { + Certificate cert = this.getSslContextFactory().getKeyStore() + .getCertificate(BoschSslUtil.getBoschShcServerId(ipAddress)); + return Base64.getEncoder().encodeToString(cert.getEncoded()); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeConfiguration.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeConfiguration.java new file mode 100644 index 0000000000000..3943f1432846a --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeConfiguration.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.bridge; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link BoschSHCBridgeConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Stefan Kästle - Initial contribution + */ +@NonNullByDefault +public class BoschSHCBridgeConfiguration { + + /** + * IP address of the Bosch Smart Home Controller + */ + public String ipAddress = ""; + + /** + * Password of the Bosch Smart Home Controller. Set during initialization via the Bosch app. + */ + public String password = ""; +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeHandler.java new file mode 100644 index 0000000000000..c5615401a51a1 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeHandler.java @@ -0,0 +1,410 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.bridge; + +import static org.eclipse.jetty.http.HttpMethod.GET; +import static org.eclipse.jetty.http.HttpMethod.PUT; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.*; +import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; +import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException; +import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException; +import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState; +import org.openhab.binding.boschshc.internal.services.dto.JsonRestExceptionResponse; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +/** + * Representation of a connection with a Bosch Smart Home Controller bridge. + * + * @author Stefan Kästle - Initial contribution + * @author Gerd Zanker - added HttpClient with pairing support + * @author Christian Oeing - refactorings of e.g. server registration + */ +@NonNullByDefault +public class BoschSHCBridgeHandler extends BaseBridgeHandler { + + private final Logger logger = LoggerFactory.getLogger(BoschSHCBridgeHandler.class); + + /** + * gson instance to convert a class to json string and back. + */ + private final Gson gson = new Gson(); + + /** + * Handler to do long polling. + */ + private final LongPolling longPolling; + + private @Nullable BoschHttpClient httpClient; + + private @Nullable ScheduledFuture scheduledPairing; + + public BoschSHCBridgeHandler(Bridge bridge) { + super(bridge); + + this.longPolling = new LongPolling(this.scheduler, this::handleLongPollResult, this::handleLongPollFailure); + } + + @Override + public void initialize() { + // Read configuration + BoschSHCBridgeConfiguration config = getConfigAs(BoschSHCBridgeConfiguration.class); + + if (config.ipAddress.isEmpty()) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No IP address set"); + return; + } + + if (config.password.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No system password set"); + return; + } + + SslContextFactory factory; + try { + // prepare SSL key and certificates + factory = new BoschSslUtil(config.ipAddress).getSslContextFactory(); + } catch (PairingFailedException e) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "@text/offline.conf-error-ssl"); + return; + } + + // Instantiate HttpClient with the SslContextFactory + BoschHttpClient httpClient = this.httpClient = new BoschHttpClient(config.ipAddress, config.password, factory); + + // Start http client + try { + httpClient.start(); + } catch (Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + String.format("Could not create http connection to controller: %s", e.getMessage())); + return; + } + + // Initialize bridge in the background. + // Start initial access the first time + scheduleInitialAccess(httpClient); + } + + @Override + public void dispose() { + // Cancel scheduled pairing. + ScheduledFuture scheduledPairing = this.scheduledPairing; + if (scheduledPairing != null) { + scheduledPairing.cancel(true); + this.scheduledPairing = null; + } + + // Stop long polling. + this.longPolling.stop(); + + BoschHttpClient httpClient = this.httpClient; + if (httpClient != null) { + try { + httpClient.stop(); + } catch (Exception e) { + logger.debug("HttpClient failed on bridge disposal: {}", e.getMessage()); + } + this.httpClient = null; + } + + super.dispose(); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + + /** + * Schedule the initial access. + * Use a delay if pairing fails and next retry is scheduled. + */ + private void scheduleInitialAccess(BoschHttpClient httpClient) { + this.scheduledPairing = scheduler.schedule(() -> initialAccess(httpClient), 15, TimeUnit.SECONDS); + } + + /** + * Execute the initial access. + * Uses the HTTP Bosch SHC client + * to check if access if possible + * pairs this Bosch SHC Bridge with the SHC if necessary + * and starts the first log poll. + */ + private void initialAccess(BoschHttpClient httpClient) { + logger.debug("Initializing Bosch SHC Bridge: {} - HTTP client is: {} - version: 2020-04-05", this, httpClient); + + try { + // check access and pair if necessary + if (!httpClient.isAccessPossible()) { + // update status already if access is not possible + this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, + "@text/offline.conf-error-pairing"); + if (!httpClient.doPairing()) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "@text/offline.conf-error-pairing"); + } + // restart initial access - needed also in case of successful pairing to check access again + scheduleInitialAccess(httpClient); + } else { + // print rooms and devices if things are reachable + boolean thingReachable = true; + thingReachable &= this.getRooms(); + thingReachable &= this.getDevices(); + + if (thingReachable) { + this.updateStatus(ThingStatus.ONLINE); + + // Start long polling + try { + this.longPolling.start(httpClient); + } catch (LongPollingFailedException e) { + this.handleLongPollFailure(e); + } + } else { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "@text/offline.not-reachable"); + // restart initial access + scheduleInitialAccess(httpClient); + } + } + } catch (InterruptedException e) { + this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, + String.format("Pairing was interrupted: %s", e.getMessage())); + } + } + + /** + * Get a list of connected devices from the Smart-Home Controller + * + * @throws InterruptedException + */ + private boolean getDevices() throws InterruptedException { + BoschHttpClient httpClient = this.httpClient; + if (httpClient == null) { + return false; + } + + try { + logger.debug("Sending http request to Bosch to request clients: {}", httpClient); + String url = httpClient.getBoschSmartHomeUrl("devices"); + ContentResponse contentResponse = httpClient.createRequest(url, GET).send(); + + String content = contentResponse.getContentAsString(); + logger.debug("Response complete: {} - return code: {}", content, contentResponse.getStatus()); + + Type collectionType = new TypeToken>() { + }.getType(); + ArrayList devices = gson.fromJson(content, collectionType); + + if (devices != null) { + for (Device d : devices) { + // Write found devices into openhab.log until we have implemented auto discovery + logger.info("Found device: name={} id={}", d.name, d.id); + if (d.deviceSerivceIDs != null) { + for (String s : d.deviceSerivceIDs) { + logger.info(".... service: {}", s); + } + } + } + } + } catch (TimeoutException | ExecutionException e) { + logger.debug("HTTP request failed with exception {}", e.getMessage()); + return false; + } + + return true; + } + + private void handleLongPollResult(LongPollResult result) { + for (DeviceStatusUpdate update : result.result) { + if (update != null && update.state != null) { + logger.debug("Got update for {}", update.deviceId); + + boolean handled = false; + + Bridge bridge = this.getThing(); + for (Thing childThing : bridge.getThings()) { + // All children of this should implement BoschSHCHandler + ThingHandler baseHandler = childThing.getHandler(); + if (baseHandler != null && baseHandler instanceof BoschSHCHandler) { + BoschSHCHandler handler = (BoschSHCHandler) baseHandler; + String deviceId = handler.getBoschID(); + + handled = true; + logger.debug("Registered device: {} - looking for {}", deviceId, update.deviceId); + + if (deviceId != null && update.deviceId.equals(deviceId)) { + logger.debug("Found child: {} - calling processUpdate with {}", handler, update.state); + handler.processUpdate(update.id, update.state); + } + } else { + logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler); + } + } + + if (!handled) { + logger.debug("Could not find a thing for device ID: {}", update.deviceId); + } + } + } + } + + private void handleLongPollFailure(Throwable e) { + logger.warn("Long polling failed", e); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Long polling failed"); + } + + /** + * Get a list of rooms from the Smart-Home controller + * + * @throws InterruptedException + */ + private boolean getRooms() throws InterruptedException { + BoschHttpClient httpClient = this.httpClient; + if (httpClient != null) { + try { + logger.debug("Sending http request to Bosch to request rooms"); + String url = httpClient.getBoschSmartHomeUrl("rooms"); + ContentResponse contentResponse = httpClient.createRequest(url, GET).send(); + + String content = contentResponse.getContentAsString(); + logger.debug("Response complete: {} - return code: {}", content, contentResponse.getStatus()); + + Type collectionType = new TypeToken>() { + }.getType(); + + ArrayList rooms = gson.fromJson(content, collectionType); + + if (rooms != null) { + for (Room r : rooms) { + logger.info("Found room: {}", r.name); + } + } + + return true; + } catch (TimeoutException | ExecutionException e) { + logger.warn("HTTP request failed: {}", e.getMessage()); + return false; + } + } else { + return false; + } + } + + /** + * Query the Bosch Smart Home Controller for the state of the given thing. + * + * @param deviceId Id of device to get state for + * @param stateName Name of the state to query + * @param stateClass Class to convert the resulting JSON to + * @throws ExecutionException + * @throws TimeoutException + * @throws InterruptedException + * @throws BoschSHCException + */ + public @Nullable T getState(String deviceId, String stateName, Class stateClass) + throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { + BoschHttpClient httpClient = this.httpClient; + if (httpClient == null) { + logger.warn("HttpClient not initialized"); + return null; + } + + String url = httpClient.getServiceUrl(stateName, deviceId); + Request request = httpClient.createRequest(url, GET).header("Accept", "application/json"); + + logger.debug("refreshState: Requesting \"{}\" from Bosch: {} via {}", stateName, deviceId, url); + + ContentResponse contentResponse = request.send(); + + String content = contentResponse.getContentAsString(); + logger.debug("refreshState: Request complete: [{}] - return code: {}", content, contentResponse.getStatus()); + + int statusCode = contentResponse.getStatus(); + if (statusCode != 200) { + JsonRestExceptionResponse errorResponse = gson.fromJson(content, JsonRestExceptionResponse.class); + if (errorResponse != null) { + throw new BoschSHCException(String.format( + "State request for service %s of device %s failed with status code %d and error code %s", + stateName, deviceId, errorResponse.statusCode, errorResponse.errorCode)); + } else { + throw new BoschSHCException( + String.format("State request for service %s of device %s failed with status code %d", stateName, + deviceId, statusCode)); + } + } + + @Nullable + T state = gson.fromJson(content, stateClass); + if (state == null) { + throw new BoschSHCException(String.format("Received invalid, expected type %s", stateClass.getName())); + } + return state; + } + + /** + * Sends a state change for a device to the controller + * + * @param deviceId Id of device to change state for + * @param serviceName Name of service of device to change state for + * @param state New state data to set for service + * + * @return Response of request + * @throws InterruptedException + * @throws ExecutionException + * @throws TimeoutException + */ + public @Nullable Response putState(String deviceId, String serviceName, T state) + throws InterruptedException, TimeoutException, ExecutionException { + BoschHttpClient httpClient = this.httpClient; + if (httpClient == null) { + logger.warn("HttpClient not initialized"); + return null; + } + + // Create request + String url = httpClient.getServiceUrl(serviceName, deviceId); + Request request = httpClient.createRequest(url, PUT, state); + + // Send request + Response response = request.send(); + return response; + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSslUtil.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSslUtil.java new file mode 100644 index 0000000000000..f23918f69278f --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSslUtil.java @@ -0,0 +1,219 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.bridge; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.Security; +import java.security.Signature; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException; +import org.openhab.core.OpenHAB; +import org.openhab.core.id.InstanceUUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * SSL context utility. + * + * @author Gerd Zanker - Initial contribution + */ +@NonNullByDefault +public class BoschSslUtil { + + private static final String OSS_OPENHAB_BINDING = "oss_openhab_binding"; + private static final String KEYSTORE_PASSWORD = "openhab"; + + private final Logger logger = LoggerFactory.getLogger(BoschSslUtil.class); + + private final String boschShcServerID; + private final String keystorePath; + + /** + * Returns unique ID for this Bosch SmartHomeController client. + * + * @return unique string containing the openhab UUID. + */ + public static String getBoschShcClientId() { + return OSS_OPENHAB_BINDING + "_" + InstanceUUID.get(); + } + + /** + * Returns ID for passed Bosch SmartHomeController server. + * + * @param shcServerID the ip address of the SHC server + * @return unique string containing the server id + */ + public static String getBoschShcServerId(String shcServerID) { + return OSS_OPENHAB_BINDING + "_" + shcServerID; + } + + /** + * Constructor + * + * @param boschShcServerID the ip address of the SHC server + */ + public BoschSslUtil(String boschShcServerID) { + this.boschShcServerID = boschShcServerID; + this.keystorePath = getKeystorePath(); + } + + /// Returns unique ID for Bosch SmartHomeController server. + public String getBoschShcServerId() { + return BoschSslUtil.getBoschShcServerId(boschShcServerID); + } + + /// Returns the unique keystore for each Bosch Smart Home Controller server. + public String getKeystorePath() { + return Paths.get(OpenHAB.getUserDataFolder(), "etc", getBoschShcServerId() + ".jks").toString(); + } + + public SslContextFactory getSslContextFactory() throws PairingFailedException { + // Instantiate and configure the SslContextFactory + SslContextFactory sslContextFactory = new SslContextFactory.Client.Client(true); // Accept all certificates + + // during pairing the cert from this keystore is accessed by HTTP client via name + sslContextFactory.setKeyStore(getKeyStoreAndCreateIfNecessary()); + + // Keystore for managing the keys that have been used to pair with the SHC + // https://www.eclipse.org/jetty/javadoc/9.4.12.v20180830/org/eclipse/jetty/util/ssl/SslContextFactory.html + sslContextFactory.setKeyStorePath(keystorePath); + sslContextFactory.setKeyStorePassword(KEYSTORE_PASSWORD); + + // Bosch is using a self signed certificate + sslContextFactory.setTrustAll(true); + sslContextFactory.setValidateCerts(false); + sslContextFactory.setValidatePeerCerts(false); + sslContextFactory.setEndpointIdentificationAlgorithm(null); + + return sslContextFactory; + } + + public KeyStore getKeyStoreAndCreateIfNecessary() throws PairingFailedException { + try { + File file = new File(keystorePath); + if (!file.exists()) { + // create new keystore + logger.info("Creating new keystore {} because it doesn't exist.", keystorePath); + return createKeyStore(keystorePath); + } else { + // load keystore as a first check + KeyStore keyStore = KeyStore.getInstance("JKS"); + try (FileInputStream keystoreStream = new FileInputStream(file)) { + keyStore.load(keystoreStream, KEYSTORE_PASSWORD.toCharArray()); + } + logger.debug("Using existing keystore {}", keystorePath); + return keyStore; + } + } catch (OperatorCreationException | GeneralSecurityException | IOException e) { + logger.debug("Exception during keystore creation {}", e.getMessage()); + throw new PairingFailedException("Can not create or load keystore file: " + keystorePath + + ". Check path, write access and JKS content.", e); + } + } + + private X509Certificate generateClientCertificate(KeyPair keyPair) + throws GeneralSecurityException, OperatorCreationException { + final String dirName = "CN=" + getBoschShcClientId() + ", O=openHAB, L=None, ST=None, C=None"; + logger.debug("Creating a new self signed certificate: {}", dirName); + final Instant now = Instant.now(); + final Date notBefore = Date.from(now); + final Date notAfter = Date.from(now.plus(Duration.ofDays(365 * 10))); + X500Name name = new X500Name(dirName); + + // create the certificate + X509v3CertificateBuilder certificateBuilder = new JcaX509v3CertificateBuilder(name, // Issuer + BigInteger.valueOf(now.toEpochMilli()), notBefore, notAfter, name, // Subject + keyPair.getPublic() // Public key to be associated with the certificate + ); + // and sign it + ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSA").build(keyPair.getPrivate()); + return new JcaX509CertificateConverter().setProvider(new BouncyCastleProvider()) + .getCertificate(certificateBuilder.build(contentSigner)); + } + + private KeyStore createKeyStore(String keystore) + throws IOException, OperatorCreationException, GeneralSecurityException { + // create a new keystore + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null, null); + + // create new key pair for BoschSHC binding + logger.debug("Creating new keypair"); + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + KeyPair keyPair = kpg.generateKeyPair(); + + Security.addProvider(new BouncyCastleProvider()); + Signature signer = Signature.getInstance("SHA256withRSA", "BC"); + signer.initSign(keyPair.getPrivate()); + signer.update("Hello openHAB".getBytes(StandardCharsets.UTF_8)); + signer.sign(); + + X509Certificate cert = generateClientCertificate(keyPair); + + logger.debug("Adding keyEntry '{}' with self signed certificate to keystore", getBoschShcServerId()); + keyStore.setKeyEntry(getBoschShcServerId(), keyPair.getPrivate(), KEYSTORE_PASSWORD.toCharArray(), + new Certificate[] { cert }); + + // add Bosch Certs + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + + logger.debug("Adding Issuing CA to keystore"); + try (BufferedInputStream streamIssuingCA = new BufferedInputStream( + this.getClass().getResourceAsStream("SmartHomeControllerIssuingCA.pem"))) { + Certificate certIssuingCA = cf.generateCertificate(streamIssuingCA); + keyStore.setCertificateEntry("Smart Home Controller Issuing CA", certIssuingCA); + } + + logger.debug("Adding root CA to keystore"); + try (BufferedInputStream streamRootCa = new BufferedInputStream( + this.getClass().getResourceAsStream("SmartHomeControllerProductiveRootCA.pem"))) { + Certificate certRooCA = cf.generateCertificate(streamRootCa); + keyStore.setCertificateEntry("Smart Home Controller Productive Root CA", certRooCA); + } + + logger.debug("Storing keystore to file {}", keystore); + try (FileOutputStream keystoreStream = new FileOutputStream(keystore)) { + keyStore.store(keystoreStream, KEYSTORE_PASSWORD.toCharArray()); + } + + return keyStore; + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/JsonRpcRequest.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/JsonRpcRequest.java new file mode 100644 index 0000000000000..ce17860db39ee --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/JsonRpcRequest.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.bridge; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Payload as POST data for triggering a RPC call on the Bosch Smart Home Controller. + * + * @author Stefan Kästle - Initial contribution + */ +@NonNullByDefault +class JsonRpcRequest { + + public String jsonrpc; + public String method; + public String[] params; + + public JsonRpcRequest(String jsonrpc, String method, String[] params) { + this.jsonrpc = jsonrpc; + this.method = method; + this.params = params; + } + + public JsonRpcRequest() { + this("", "", new String[0]); + } + + public String getJsonrpc() { + return jsonrpc; + } + + public void setJsonrpc(String jsonrpc) { + this.jsonrpc = jsonrpc; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public String[] getParams() { + return params; + } + + public void setParams(String[] params) { + this.params = params; + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java new file mode 100644 index 0000000000000..0c35b91b2c144 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java @@ -0,0 +1,211 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.bridge; + +import static org.eclipse.jetty.http.HttpMethod.POST; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.client.util.BufferingResponseListener; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollError; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult; +import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; +import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; + +/** + * Handles the long polling to the Smart Home Controller. + * + * @author Christian Oeing - Initial contribution + */ +@NonNullByDefault +public class LongPolling { + + private final Logger logger = LoggerFactory.getLogger(LongPolling.class); + + /** + * gson instance to convert a class to json string and back. + */ + private final Gson gson = new Gson(); + + /** + * Executor to schedule long polls. + */ + private final ScheduledExecutorService scheduler; + + /** + * Handler for long poll results. + */ + private final Consumer handleResult; + + /** + * Handler for unrecoverable. + */ + private final Consumer handleFailure; + + /** + * Current running long polling request. + */ + private @Nullable Request request; + + /** + * Indicates if long polling was aborted. + */ + private boolean aborted = false; + + public LongPolling(ScheduledExecutorService scheduler, Consumer handleResult, + Consumer handleFailure) { + this.scheduler = scheduler; + this.handleResult = handleResult; + this.handleFailure = handleFailure; + } + + public void start(BoschHttpClient httpClient) throws LongPollingFailedException { + // Subscribe to state updates. + String subscriptionId = this.subscribe(httpClient); + this.executeLongPoll(httpClient, subscriptionId); + } + + public void stop() { + // Abort long polling. + this.aborted = true; + Request request = this.request; + if (request != null) { + request.abort(new AbortLongPolling()); + this.request = null; + } + } + + /** + * Subscribe to events and store the subscription ID needed for long polling. + * + * @param httpClient Http client to use for sending subscription request + * @return Subscription id + */ + private String subscribe(BoschHttpClient httpClient) throws LongPollingFailedException { + try { + String url = httpClient.getBoschShcUrl("remote/json-rpc"); + JsonRpcRequest request = new JsonRpcRequest("2.0", "RE/subscribe", + new String[] { "com/bosch/sh/remote/*", null }); + logger.debug("Subscribe: Sending request: {} - using httpClient {}", gson.toJson(request), httpClient); + Request httpRequest = httpClient.createRequest(url, POST, request); + SubscribeResult response = httpClient.sendRequest(httpRequest, SubscribeResult.class); + + logger.debug("Subscribe: Got subscription ID: {} {}", response.getResult(), response.getJsonrpc()); + String subscriptionId = response.getResult(); + return subscriptionId; + } catch (TimeoutException | ExecutionException | InterruptedException e) { + throw new LongPollingFailedException("Error on subscribe request", e); + } + } + + private void executeLongPoll(BoschHttpClient httpClient, String subscriptionId) { + scheduler.execute(() -> this.longPoll(httpClient, subscriptionId)); + } + + /** + * Start long polling the home controller. Once a long poll resolves, a new one is started. + */ + private void longPoll(BoschHttpClient httpClient, String subscriptionId) { + logger.debug("Sending long poll request"); + + JsonRpcRequest requestContent = new JsonRpcRequest("2.0", "RE/longPoll", new String[] { subscriptionId, "20" }); + String url = httpClient.getBoschShcUrl("remote/json-rpc"); + Request request = httpClient.createRequest(url, POST, requestContent); + + // Long polling responds after 20 seconds with an empty response if no update has happened. + // 10 second threshold was added to not time out if response from controller takes a bit longer than 20 seconds. + request.timeout(30, TimeUnit.SECONDS); + + this.request = request; + LongPolling longPolling = this; + request.send(new BufferingResponseListener() { + @Override + public void onComplete(@Nullable Result result) { + Throwable failure = result != null ? result.getFailure() : null; + if (failure != null) { + if (failure instanceof ExecutionException) { + if (failure.getCause() instanceof AbortLongPolling) { + logger.debug("Canceling long polling for subscription id {} because it was aborted", + subscriptionId); + } else { + longPolling.handleFailure.accept(new LongPollingFailedException( + "Unexpected exception during long polling request", failure)); + } + } else { + longPolling.handleFailure.accept(new LongPollingFailedException( + "Unexpected exception during long polling request", failure)); + } + } else { + longPolling.onLongPollResponse(httpClient, subscriptionId, this.getContentAsString()); + } + } + }); + } + + private void onLongPollResponse(BoschHttpClient httpClient, String subscriptionId, String content) { + // Check if thing is still online + if (this.aborted) { + logger.debug("Canceling long polling for subscription id {} because it was aborted", subscriptionId); + return; + } + + logger.debug("Long poll response: {}", content); + + String nextSubscriptionId = subscriptionId; + + LongPollResult longPollResult = gson.fromJson(content, LongPollResult.class); + if (longPollResult != null && longPollResult.result != null) { + this.handleResult.accept(longPollResult); + } else { + logger.warn("Long poll response contained no results: {}", content); + + // Check if we got a proper result from the SHC + LongPollError longPollError = gson.fromJson(content, LongPollError.class); + + if (longPollError != null && longPollError.error != null) { + logger.warn("Got long poll error: {} (code: {})", longPollError.error.message, + longPollError.error.code); + + if (longPollError.error.code == LongPollError.SUBSCRIPTION_INVALID) { + logger.warn("Subscription {} became invalid, subscribing again", subscriptionId); + try { + nextSubscriptionId = this.subscribe(httpClient); + } catch (LongPollingFailedException e) { + this.handleFailure.accept(e); + return; + } + } + } + } + + // Execute next run. + this.executeLongPoll(httpClient, nextSubscriptionId); + } + + @SuppressWarnings("serial") + private class AbortLongPolling extends BoschSHCException { + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Device.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Device.java new file mode 100644 index 0000000000000..562e075ed3dd3 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Device.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.bridge.dto; + +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +/** + * Represents a single devices connected to the Bosch Smart Home Controller. + * + * Example from Json: + * + * { + * "@type":"device", + * "rootDeviceId":"64-da-a0-02-14-9b", + * "id":"hdm:HomeMaticIP:3014F711A00004953859F31B", + * "deviceServiceIds":["PowerMeter","PowerSwitch","PowerSwitchProgram","Routing"], + * "manufacturer":"BOSCH", + * "roomId":"hz_3", + * "deviceModel":"PSM", + * "serial":"3014F711A00004953859F31B", + * "profile":"GENERIC", + * "name":"Coffee Machine", + * "status":"AVAILABLE", + * "childDeviceIds":[] + * } + * + * @author Stefan Kästle - Initial contribution + */ +public class Device { + + @SerializedName("@type") + public String type; + + public String rootDeviceId; + public String id; + public List deviceSerivceIDs; + public String manufacturer; + public String roomId; + public String deviceModel; + public String serial; + public String profile; + public String name; + public String status; + public List childDeviceIds; +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/DeviceStatusUpdate.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/DeviceStatusUpdate.java new file mode 100644 index 0000000000000..649f3de5bfda4 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/DeviceStatusUpdate.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.bridge.dto; + +import com.google.gson.JsonElement; +import com.google.gson.annotations.SerializedName; + +/** + * Represents a device status update as represented by the Smart Home + * Controller. + * + * @author Stefan Kästle - Initial contribution + * @author Christian Oeing - refactorings of e.g. server registration + */ +public class DeviceStatusUpdate { + /** + * Url path of the service the update came from. + */ + public String path; + + /** + * The type of message. + */ + @SerializedName("@type") + public String type; + + /** + * Name of service the update came from. + */ + public String id; + + /** + * Current state of device. Serialized as JSON. + */ + public JsonElement state; + + /** + * Id of device the update is for. + */ + public String deviceId; + + @Override + public String toString() { + return this.deviceId + "state: " + this.type; + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollError.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollError.java new file mode 100644 index 0000000000000..ca7df8ceee161 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollError.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.bridge.dto; + +/** + * Error response of the Controller for a Long Poll API call. + * + * @author Stefan Kästle - Initial contribution + */ +public class LongPollError { + + public static final int SUBSCRIPTION_INVALID = -32001; + + /** + * { + * "jsonrpc":"2.0", + * "error": { + * "code":-32001, + * "message":"No subscription with id: e8fei62b0-0" + * } + * } + */ + + public class ErrorInfo { + public int code; + public String message; + } + + public String jsonrpc; + public ErrorInfo error; +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResult.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResult.java new file mode 100644 index 0000000000000..3a6bf2b4df823 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResult.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.bridge.dto; + +import java.util.ArrayList; + +/** + * Response of the Controller for a Long Poll API call. + * + * @author Stefan Kästle - Initial contribution + */ +public class LongPollResult { + + /** + * {"result":[ + * ..{ + * ...."path":"/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch", + * ...."@type":"DeviceServiceData", + * ...."id":"PowerSwitch", + * ...."state":{ + * ......"@type":"powerSwitchState", + * ......"switchState":"ON" + * ....}, + * ...."deviceId":"hdm:HomeMaticIP:3014F711A0001916D859A8A9"} + * ],"jsonrpc":"2.0"} + */ + + public ArrayList result; + public String jsonrpc; +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Room.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Room.java new file mode 100644 index 0000000000000..fbfb62486536a --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Room.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.bridge.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * A room as represented by the controller. + * + * Json example: + * {"@type":"room","id":"hz_1","iconId":"icon_room_bedroom","name":"Bedroom"} + * + * @author Stefan Kästle - Initial contribution + */ +public class Room { + + @SerializedName("@type") + public String type; + + public String id; + public String name; +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/SubscribeResult.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/SubscribeResult.java new file mode 100644 index 0000000000000..c0df59e65480c --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/SubscribeResult.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.bridge.dto; + +/** + * Response of the Controller for a Long Poll API call. + * + * The result field will contain the subscription ID needed for further API calls (e.g. the long polling call) + * + * @author Stefan Kästle - Initial contribution + */ +public class SubscribeResult { + private String result; + private String jsonrpc; + + public String getResult() { + return this.result; + } + + public String getJsonrpc() { + return this.jsonrpc; + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/climatecontrol/ClimateControlHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/climatecontrol/ClimateControlHandler.java new file mode 100644 index 0000000000000..1b6a96a09a4bb --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/climatecontrol/ClimateControlHandler.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.climatecontrol; + +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_SETPOINT_TEMPERATURE; +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler; +import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; +import org.openhab.binding.boschshc.internal.services.roomclimatecontrol.RoomClimateControlService; +import org.openhab.binding.boschshc.internal.services.roomclimatecontrol.dto.RoomClimateControlServiceState; +import org.openhab.binding.boschshc.internal.services.temperaturelevel.TemperatureLevelService; +import org.openhab.binding.boschshc.internal.services.temperaturelevel.dto.TemperatureLevelServiceState; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; + +/** + * A virtual device which controls up to six Bosch Smart Home radiator thermostats in a room. + * + * @author Christian Oeing - Initial contribution + */ +@NonNullByDefault +public final class ClimateControlHandler extends BoschSHCHandler { + + private RoomClimateControlService roomClimateControlService; + + /** + * Constructor. + * + * @param thing The Bosch Smart Home device that should be handled. + */ + public ClimateControlHandler(Thing thing) { + super(thing); + this.roomClimateControlService = new RoomClimateControlService(); + } + + @Override + protected void initializeServices() throws BoschSHCException { + super.createService(TemperatureLevelService::new, this::updateChannels, List.of(CHANNEL_TEMPERATURE)); + super.registerService(this.roomClimateControlService, this::updateChannels, + List.of(CHANNEL_SETPOINT_TEMPERATURE)); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + switch (channelUID.getId()) { + case CHANNEL_SETPOINT_TEMPERATURE: + if (command instanceof QuantityType) { + updateSetpointTemperature((QuantityType) command); + } + break; + } + } + + /** + * Updates the channels which are linked to the {@link TemperatureLevelService} of the device. + * + * @param state Current state of {@link TemperatureLevelService}. + */ + private void updateChannels(TemperatureLevelServiceState state) { + super.updateState(CHANNEL_TEMPERATURE, state.getTemperatureState()); + } + + /** + * Updates the channels which are linked to the {@link RoomClimateControlService} of the device. + * + * @param state Current state of {@link RoomClimateControlService}. + */ + private void updateChannels(RoomClimateControlServiceState state) { + super.updateState(CHANNEL_SETPOINT_TEMPERATURE, state.getSetpointTemperatureState()); + } + + /** + * Sets the desired temperature for the device. + * + * @param quantityType Command which contains the new desired temperature. + */ + private void updateSetpointTemperature(QuantityType quantityType) { + QuantityType celsiusType = quantityType.toUnit(SIUnits.CELSIUS); + if (celsiusType == null) { + logger.debug("Could not convert quantity command to celsius"); + return; + } + + double setpointTemperature = celsiusType.doubleValue(); + this.updateServiceState(this.roomClimateControlService, + new RoomClimateControlServiceState(setpointTemperature)); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/inwallswitch/BoschInWallSwitchHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/inwallswitch/BoschInWallSwitchHandler.java new file mode 100644 index 0000000000000..b5a4ebd449ca5 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/inwallswitch/BoschInWallSwitchHandler.java @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.inwallswitch; + +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*; + +import java.util.List; + +import javax.measure.quantity.Energy; +import javax.measure.quantity.Power; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler; +import org.openhab.binding.boschshc.internal.devices.inwallswitch.dto.PowerMeterState; +import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; +import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchService; +import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState; +import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; + +import com.google.gson.JsonElement; + +/** + * Represents Bosch in-wall switches. + * + * @author Stefan Kästle - Initial contribution + */ +@NonNullByDefault +public class BoschInWallSwitchHandler extends BoschSHCHandler { + + private final PowerSwitchService powerSwitchService; + + public BoschInWallSwitchHandler(Thing thing) { + super(thing); + this.powerSwitchService = new PowerSwitchService(); + } + + @Override + protected void initializeServices() throws BoschSHCException { + super.initializeServices(); + + this.registerService(this.powerSwitchService, this::updateChannels, List.of(CHANNEL_POWER_SWITCH)); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + + logger.debug("Handle command for: {} - {}", channelUID.getThingUID(), command); + + if (command instanceof RefreshType) { + switch (channelUID.getId()) { + case CHANNEL_POWER_CONSUMPTION: { + PowerMeterState state = this.getState("PowerMeter", PowerMeterState.class); + if (state != null) { + updatePowerMeterState(state); + } + break; + } + case CHANNEL_ENERGY_CONSUMPTION: + // Nothing to do here, since the same update is received from POWER_CONSUMPTION + break; + default: + logger.warn("Received refresh request for unsupported channel: {}", channelUID); + } + } else { + switch (channelUID.getId()) { + case CHANNEL_POWER_SWITCH: + if (command instanceof OnOffType) { + updatePowerSwitchState((OnOffType) command); + } + break; + } + } + } + + void updatePowerMeterState(PowerMeterState state) { + logger.debug("Parsed power meter state of {}: energy {} - power {}", this.getBoschID(), state.energyConsumption, + state.energyConsumption); + + updateState(CHANNEL_POWER_CONSUMPTION, new QuantityType(state.powerConsumption, Units.WATT)); + updateState(CHANNEL_ENERGY_CONSUMPTION, new QuantityType(state.energyConsumption, Units.WATT_HOUR)); + } + + /** + * Updates the channels which are linked to the {@link PowerSwitchService} of the device. + * + * @param state Current state of {@link PowerSwitchService}. + */ + private void updateChannels(PowerSwitchServiceState state) { + State powerState = OnOffType.from(state.switchState.toString()); + super.updateState(CHANNEL_POWER_SWITCH, powerState); + } + + private void updatePowerSwitchState(OnOffType command) { + PowerSwitchServiceState state = new PowerSwitchServiceState(); + state.switchState = PowerSwitchState.valueOf(command.toFullString()); + this.updateServiceState(this.powerSwitchService, state); + } + + @Override + public void processUpdate(String id, JsonElement state) { + super.processUpdate(id, state); + + logger.debug("in-wall switch: received update: ID {} state {}", id, state); + + if (id.equals("PowerMeter")) { + PowerMeterState powerMeterState = GSON.fromJson(state, PowerMeterState.class); + if (powerMeterState == null) { + logger.warn("Received unknown update in in-wall switch: {}", state); + } else { + updatePowerMeterState(powerMeterState); + } + } + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/inwallswitch/dto/PowerMeterState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/inwallswitch/dto/PowerMeterState.java new file mode 100644 index 0000000000000..9e0ba54f824ed --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/inwallswitch/dto/PowerMeterState.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.inwallswitch.dto; + +import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState; + +/** + * PowerMeterState + * + * @author Stefan Kästle - Initial contribution + */ +public class PowerMeterState extends BoschSHCServiceState { + + public PowerMeterState() { + super("powerMeterState"); + } + + public double energyConsumption; + public double powerConsumption; +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/MotionDetectorHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/MotionDetectorHandler.java new file mode 100644 index 0000000000000..6e3f564fe7f1e --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/MotionDetectorHandler.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.motiondetector; + +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_LATEST_MOTION; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler; +import org.openhab.binding.boschshc.internal.devices.motiondetector.dto.LatestMotionState; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; + +import com.google.gson.JsonElement; + +/** + * MotionDetectorHandler + * + * @author Stefan Kästle - Initial contribution + */ +@NonNullByDefault +public class MotionDetectorHandler extends BoschSHCHandler { + + public MotionDetectorHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("Handle command for: {} - {}", channelUID.getThingUID(), command); + + if (CHANNEL_LATEST_MOTION.equals(channelUID.getId())) { + if (command instanceof RefreshType) { + LatestMotionState state = this.getState("LatestMotion", LatestMotionState.class); + if (state != null) { + updateLatestMotionState(state); + } + } + } + } + + void updateLatestMotionState(LatestMotionState state) { + DateTimeType date = new DateTimeType(state.latestMotionDetected); + updateState(CHANNEL_LATEST_MOTION, date); + } + + @Override + public void processUpdate(String id, JsonElement state) { + logger.debug("Motion detector: received update: {} {}", id, state); + + @Nullable + LatestMotionState latestMotionState = GSON.fromJson(state, LatestMotionState.class); + if (latestMotionState == null) { + logger.warn("Received unknown update in in-wall switch: {}", state); + return; + } + updateLatestMotionState(latestMotionState); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/dto/LatestMotionState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/dto/LatestMotionState.java new file mode 100644 index 0000000000000..38658342c3b88 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/dto/LatestMotionState.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.motiondetector.dto; + +import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState; + +/** + * { + * "result": [ + * { + * "path": "/devices/hdm:ZigBee:000d6f0004b95a62/services/LatestMotion", + * "@type": "DeviceServiceData", + * "id": "LatestMotion", + * "state": { + * "latestMotionDetected": "2020-04-03T19:02:19.054Z", + * "@type": "latestMotionState" + * }, + * "deviceId": "hdm:ZigBee:000d6f0004b95a62" + * } + * ], + * "jsonrpc": "2.0" + * } + * + * @author Stefan Kästle - Initial contribution + */ +public class LatestMotionState extends BoschSHCServiceState { + + public LatestMotionState() { + super("latestMotionState"); + } + + public String latestMotionDetected; +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/shuttercontrol/ShutterControlHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/shuttercontrol/ShutterControlHandler.java new file mode 100644 index 0000000000000..6b54503744e20 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/shuttercontrol/ShutterControlHandler.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.shuttercontrol; + +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_LEVEL; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler; +import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; +import org.openhab.binding.boschshc.internal.services.shuttercontrol.OperationState; +import org.openhab.binding.boschshc.internal.services.shuttercontrol.ShutterControlService; +import org.openhab.binding.boschshc.internal.services.shuttercontrol.dto.ShutterControlServiceState; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.StopMoveType; +import org.openhab.core.library.types.UpDownType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; + +/** + * Handler for a shutter control device + * + * @author Christian Oeing - Initial contribution + */ +@NonNullByDefault +public class ShutterControlHandler extends BoschSHCHandler { + /** + * Utility functions to convert data between Bosch things and openHAB items + */ + static final class DataConversion { + public static int levelToOpenPercentage(double level) { + return (int) Math.round((1 - level) * 100); + } + + public static double openPercentageToLevel(double openPercentage) { + return (100 - openPercentage) / 100.0; + } + } + + private ShutterControlService shutterControlService; + + public ShutterControlHandler(Thing thing) { + super(thing); + this.shutterControlService = new ShutterControlService(); + } + + @Override + protected void initializeServices() throws BoschSHCException { + super.initializeServices(); + + this.registerService(this.shutterControlService, this::updateChannels, List.of(CHANNEL_LEVEL)); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + + if (command instanceof UpDownType) { + // Set full close/open as target state + UpDownType upDownType = (UpDownType) command; + ShutterControlServiceState state = new ShutterControlServiceState(); + if (upDownType == UpDownType.UP) { + state.level = 1.0; + } else if (upDownType == UpDownType.DOWN) { + state.level = 0.0; + } else { + logger.warn("Received unknown UpDownType command: {}", upDownType); + return; + } + this.updateServiceState(this.shutterControlService, state); + } else if (command instanceof StopMoveType) { + StopMoveType stopMoveType = (StopMoveType) command; + if (stopMoveType == StopMoveType.STOP) { + // Set STOPPED operation state + ShutterControlServiceState state = new ShutterControlServiceState(); + state.operationState = OperationState.STOPPED; + this.updateServiceState(this.shutterControlService, state); + } + } else if (command instanceof PercentType) { + // Set specific level + PercentType percentType = (PercentType) command; + double level = DataConversion.openPercentageToLevel(percentType.doubleValue()); + this.updateServiceState(this.shutterControlService, new ShutterControlServiceState(level)); + } + } + + private void updateChannels(ShutterControlServiceState state) { + if (state.level != null) { + // Convert level to open ratio + int openPercentage = DataConversion.levelToOpenPercentage(state.level); + updateState(CHANNEL_LEVEL, new PercentType(openPercentage)); + } + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandler.java new file mode 100644 index 0000000000000..5f71040558522 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandler.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.thermostat; + +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE; +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_VALVE_TAPPET_POSITION; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler; +import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; +import org.openhab.binding.boschshc.internal.services.temperaturelevel.TemperatureLevelService; +import org.openhab.binding.boschshc.internal.services.temperaturelevel.dto.TemperatureLevelServiceState; +import org.openhab.binding.boschshc.internal.services.valvetappet.ValveTappetService; +import org.openhab.binding.boschshc.internal.services.valvetappet.dto.ValveTappetServiceState; +import org.openhab.core.thing.Thing; + +/** + * Handler for a thermostat device. + * + * @author Christian Oeing - Initial contribution + */ +@NonNullByDefault +public final class ThermostatHandler extends BoschSHCHandler { + + public ThermostatHandler(Thing thing) { + super(thing); + } + + @Override + protected void initializeServices() throws BoschSHCException { + this.createService(TemperatureLevelService::new, this::updateChannels, List.of(CHANNEL_TEMPERATURE)); + this.createService(ValveTappetService::new, this::updateChannels, List.of(CHANNEL_VALVE_TAPPET_POSITION)); + } + + /** + * Updates the channels which are linked to the {@link TemperatureLevelService} of the device. + * + * @param state Current state of {@link TemperatureLevelService}. + */ + private void updateChannels(TemperatureLevelServiceState state) { + super.updateState(CHANNEL_TEMPERATURE, state.getTemperatureState()); + } + + /** + * Updates the channels which are linked to the {@link ValveTappetService} of the device. + * + * @param state Current state of {@link ValveTappetService}. + */ + private void updateChannels(ValveTappetServiceState state) { + super.updateState(CHANNEL_VALVE_TAPPET_POSITION, state.getPositionState()); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/BoschTwinguardHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/BoschTwinguardHandler.java new file mode 100644 index 0000000000000..d5d58312075ac --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/BoschTwinguardHandler.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.twinguard; + +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*; + +import javax.measure.quantity.Dimensionless; +import javax.measure.quantity.Temperature; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler; +import org.openhab.binding.boschshc.internal.devices.twinguard.dto.AirQualityLevelState; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; + +import com.google.gson.JsonElement; +import com.google.gson.JsonSyntaxException; + +/** + * The {@link BoschSHCHandler} is responsible for handling commands for the TwinGuard handler. + * + * @author Stefan Kästle - Initial contribution + */ +@NonNullByDefault +public class BoschTwinguardHandler extends BoschSHCHandler { + + public BoschTwinguardHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + Bridge bridge = this.getBridge(); + + if (bridge != null) { + logger.debug("Handle command for: {} - {}", channelUID.getThingUID(), command); + + if (command instanceof RefreshType) { + AirQualityLevelState state = this.getState("AirQualityLevel", AirQualityLevelState.class); + if (state != null) { + updateAirQualityState(state); + } + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Bridge is NUL"); + } + } + + void updateAirQualityState(AirQualityLevelState state) { + updateState(CHANNEL_TEMPERATURE, new QuantityType(state.temperature, SIUnits.CELSIUS)); + updateState(CHANNEL_TEMPERATURE_RATING, new StringType(state.temperatureRating)); + updateState(CHANNEL_HUMIDITY, new QuantityType(state.humidity, Units.ONE)); + updateState(CHANNEL_HUMIDITY_RATING, new StringType(state.humidityRating)); + updateState(CHANNEL_PURITY, new QuantityType(state.purity, Units.ONE)); + updateState(CHANNEL_AIR_DESCRIPTION, new StringType(state.description)); + updateState(CHANNEL_PURITY_RATING, new StringType(state.purityRating)); + updateState(CHANNEL_COMBINED_RATING, new StringType(state.combinedRating)); + } + + @Override + public void processUpdate(String id, JsonElement state) throws JsonSyntaxException { + logger.debug("Twinguard: received update: {} {}", id, state); + + AirQualityLevelState parsed = GSON.fromJson(state, AirQualityLevelState.class); + if (parsed == null) { + logger.warn("Received unknown update in in-wall switch: {}", state); + return; + } + + logger.debug("Parsed switch state of {}: {}", this.getBoschID(), parsed); + updateAirQualityState(parsed); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/dto/AirQualityLevelState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/dto/AirQualityLevelState.java new file mode 100644 index 0000000000000..724377b53e011 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/dto/AirQualityLevelState.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.twinguard.dto; + +import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState; + +/** + * Represents the state of a device as reported from the Smart Home Controller + * + * @author Stefan Kästle - Initial contribution + */ +public class AirQualityLevelState extends BoschSHCServiceState { + + public AirQualityLevelState() { + super("airQualityLevelState"); + } + + /* + * {"maxTemperature":25,"minTemperature":20,"custom":false,"name":"HALLWAY","maxHumidity":60,"minHumidity":40, + * "maxPurity":1000} + */ + class ComfortZone { + double maxTemperature; + double minTemperature; + boolean custom; + String name; + double maxHumidity; + double minHumidity; + double maxPurity; + } + + /** + * {"temperatureRating":"GOOD","humidityRating":"MEDIUM","purity":620,"comfortZone":....,"@type":"airQualityLevelState", + * "purityRating":"GOOD","temperature":23.77,"description":"LITTLE_DRY","humidity":32.69,"combinedRating":"MEDIUM"} + */ + + public String temperatureRating; + public String humidityRating; + + public int purity; + + public ComfortZone comfortZone; + + public String purityRating; + + public double temperature; + public String description; + + public double humidity; + public String combinedRating; +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/windowcontact/WindowContactHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/windowcontact/WindowContactHandler.java new file mode 100644 index 0000000000000..90b978a20430d --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/windowcontact/WindowContactHandler.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.windowcontact; + +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_CONTACT; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler; +import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; +import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactService; +import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactState; +import org.openhab.binding.boschshc.internal.services.shuttercontact.dto.ShutterContactServiceState; +import org.openhab.core.library.types.OpenClosedType; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.State; + +/** + * The {@link BoschSHCHandler} is responsible for handling Bosch window/door contacts. + * + * @author Stefan Kästle - Initial contribution + */ +@NonNullByDefault +public class WindowContactHandler extends BoschSHCHandler { + + public WindowContactHandler(Thing thing) { + super(thing); + } + + @Override + protected void initializeServices() throws BoschSHCException { + this.createService(ShutterContactService::new, this::updateChannels, List.of(CHANNEL_CONTACT)); + } + + private void updateChannels(ShutterContactServiceState state) { + State contact = state.value == ShutterContactState.CLOSED ? OpenClosedType.CLOSED : OpenClosedType.OPEN; + updateState(CHANNEL_CONTACT, contact); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/BoschSHCException.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/BoschSHCException.java new file mode 100644 index 0000000000000..74fa72e33135b --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/BoschSHCException.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.exceptions; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Exception class for Bosch Smart Home controller errors. + * + * @author Gerd Zanker - Initial contribution + */ +@SuppressWarnings("serial") +@NonNullByDefault +public class BoschSHCException extends Exception { + public BoschSHCException() { + } + + public BoschSHCException(String message) { + super(message); + } + + public BoschSHCException(String message, Throwable e) { + super(message, e); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/LongPollingFailedException.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/LongPollingFailedException.java new file mode 100644 index 0000000000000..72ae10365bd05 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/LongPollingFailedException.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.exceptions; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Thrown if the long polling failed + * + * @author Christian Oeing - Initial contribution + */ +@SuppressWarnings("serial") +@NonNullByDefault +public class LongPollingFailedException extends BoschSHCException { + public LongPollingFailedException(String message, Throwable e) { + super(message, e); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/PairingFailedException.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/PairingFailedException.java new file mode 100644 index 0000000000000..50cdfaf369c02 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/exceptions/PairingFailedException.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.exceptions; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Thrown if the pairing failed multiple times + * + * @author Gerd Zanker - Initial contribution + */ +@SuppressWarnings("serial") +@NonNullByDefault +public class PairingFailedException extends BoschSHCException { + public PairingFailedException() { + } + + public PairingFailedException(String message) { + super(message); + } + + public PairingFailedException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/BoschSHCService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/BoschSHCService.java new file mode 100644 index 0000000000000..b58d1c3240656 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/BoschSHCService.java @@ -0,0 +1,198 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.boschshc.internal.devices.bridge.BoschSHCBridgeHandler; +import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; +import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; + +/** + * Base class of a service of a Bosch Smart Home device. + * The services of the devices and their official APIs can be found here: https://apidocs.bosch-smarthome.com/local/ + * + * @author Christian Oeing - Initial contribution + */ +@NonNullByDefault +public abstract class BoschSHCService { + + private final Logger logger = LoggerFactory.getLogger(BoschSHCService.class); + + /** + * Unique service name + */ + private final String serviceName; + + /** + * Class of service state + */ + private final Class stateClass; + + /** + * gson instance to convert a class to json string and back. + */ + private final Gson gson = new Gson(); + + /** + * Bridge to use for communication from/to the device + */ + private @Nullable BoschSHCBridgeHandler bridgeHandler; + + /** + * Id of device the service belongs to + */ + private @Nullable String deviceId; + + /** + * Function to call after receiving state updates from the device + */ + private @Nullable Consumer stateUpdateListener; + + /** + * Constructor + * + * @param serviceName Unique name of the service. + * @param stateClass State class that this service uses for data transfers from/to the device. + */ + protected BoschSHCService(String serviceName, Class stateClass) { + this.serviceName = serviceName; + this.stateClass = stateClass; + } + + /** + * Initializes the service + * + * @param bridgeHandler Bridge to use for communication from/to the device + * @param deviceId Id of device this service is for + * @param stateUpdateListener Function to call when a state update was received from the device. + */ + public void initialize(BoschSHCBridgeHandler bridgeHandler, String deviceId, + @Nullable Consumer stateUpdateListener) { + this.bridgeHandler = bridgeHandler; + this.deviceId = deviceId; + this.stateUpdateListener = stateUpdateListener; + } + + /** + * Returns the unique name of this service. + * + * @return Unique name of the service. + */ + public String getServiceName() { + return this.serviceName; + } + + /** + * Returns the class of the state this service provides. + * + * @return Class of the state this service provides. + */ + public Class getStateClass() { + return this.stateClass; + } + + /** + * Requests the current state of the service and updates it. + * + * @throws ExecutionException + * @throws TimeoutException + * @throws InterruptedException + * @throws BoschSHCException + */ + public void refreshState() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { + @Nullable + TState state = this.getState(); + if (state != null) { + this.onStateUpdate(state); + } + } + + /** + * Requests the current state of the device with the specified id. + * + * @return Current state of the device. + * @throws ExecutionException + * @throws TimeoutException + * @throws InterruptedException + * @throws BoschSHCException + */ + public @Nullable TState getState() + throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { + String deviceId = this.deviceId; + if (deviceId == null) { + return null; + } + BoschSHCBridgeHandler bridgeHandler = this.bridgeHandler; + if (bridgeHandler == null) { + return null; + } + return bridgeHandler.getState(deviceId, this.serviceName, this.stateClass); + } + + /** + * Sets the state of the device with the specified id. + * + * @param state State to set. + * @throws InterruptedException + * @throws ExecutionException + * @throws TimeoutException + */ + public void setState(TState state) throws InterruptedException, TimeoutException, ExecutionException { + String deviceId = this.deviceId; + if (deviceId == null) { + return; + } + BoschSHCBridgeHandler bridgeHandler = this.bridgeHandler; + if (bridgeHandler == null) { + return; + } + bridgeHandler.putState(deviceId, this.serviceName, state); + } + + /** + * A state update was received from the bridge + * + * @param stateData Current state of service. Serialized as JSON. + */ + public void onStateUpdate(JsonElement stateData) { + @Nullable + TState state = gson.fromJson(stateData, this.stateClass); + if (state == null) { + this.logger.warn("Received invalid, expected type {}", this.stateClass.getName()); + return; + } + this.onStateUpdate(state); + } + + /** + * A state update was received from the bridge. + * + * @param state Current state of service as an instance of the state class. + */ + private void onStateUpdate(TState state) { + Consumer stateUpdateListener = this.stateUpdateListener; + if (stateUpdateListener != null) { + stateUpdateListener.accept(state); + } + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceState.java new file mode 100644 index 0000000000000..0ffbfaf14859e --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceState.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services.dto; + +import com.google.gson.annotations.SerializedName; + +/** + * Base Bosch Smart Home Controller service state. + * + * @author Christian Oeing - Initial contribution + */ +public class BoschSHCServiceState { + @SerializedName("@type") + private final String type; + + protected BoschSHCServiceState(String type) { + this.type = type; + } + + public String getType() { + return type; + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/JsonRestExceptionResponse.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/JsonRestExceptionResponse.java new file mode 100644 index 0000000000000..cb8994bad4442 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/JsonRestExceptionResponse.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services.dto; + +/** + * Generic error response of the Bosch REST API. + * + * @author Christian Oeing - Initial contribution + */ +public class JsonRestExceptionResponse extends BoschSHCServiceState { + public JsonRestExceptionResponse() { + super("JsonRestExceptionResponseEntity"); + this.errorCode = ""; + this.statusCode = 0; + } + + /** + * The error code of the occurred Exception. + */ + public String errorCode; + + /** + * The HTTP status of the error. + */ + public int statusCode; +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchService.java new file mode 100644 index 0000000000000..622fac8009bfb --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchService.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services.powerswitch; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.services.BoschSHCService; +import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState; + +/** + * Service to get and set the state of a power switch. + * + * @author Christian Oeing - Initial contribution + */ +@NonNullByDefault +public class PowerSwitchService extends BoschSHCService { + + public PowerSwitchService() { + super("PowerSwitch", PowerSwitchServiceState.class); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchState.java new file mode 100644 index 0000000000000..236bcf61531cb --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchState.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services.powerswitch; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Possible states of a power switch. + * + * @author Christian Oeing - Initial contribution + */ +@NonNullByDefault +public enum PowerSwitchState { + ON, + OFF +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/dto/PowerSwitchServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/dto/PowerSwitchServiceState.java new file mode 100644 index 0000000000000..d75a47609805c --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/dto/PowerSwitchServiceState.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services.powerswitch.dto; + +import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState; +import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState; + +/** + * Represents the state of a power switch device as reported from the Smart Home Controller + * + * @author Stefan Kästle - Initial contribution + * @author Christian Oeing - Adjustments to match general service state structure + */ +public class PowerSwitchServiceState extends BoschSHCServiceState { + + public PowerSwitchServiceState() { + super("powerSwitchState"); + } + + /** + * Current state of power switch. + */ + public PowerSwitchState switchState; +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/roomclimatecontrol/RoomClimateControlService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/roomclimatecontrol/RoomClimateControlService.java new file mode 100644 index 0000000000000..b84b39dde81b1 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/roomclimatecontrol/RoomClimateControlService.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services.roomclimatecontrol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.services.BoschSHCService; +import org.openhab.binding.boschshc.internal.services.roomclimatecontrol.dto.RoomClimateControlServiceState; + +/** + * Service of a virtual device which controls the radiator thermostats in a room. + * + * @author Christian Oeing - Initial contribution + */ +@NonNullByDefault +public class RoomClimateControlService extends BoschSHCService { + public RoomClimateControlService() { + super("RoomClimateControl", RoomClimateControlServiceState.class); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/roomclimatecontrol/dto/RoomClimateControlServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/roomclimatecontrol/dto/RoomClimateControlServiceState.java new file mode 100644 index 0000000000000..bd5a261d60836 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/roomclimatecontrol/dto/RoomClimateControlServiceState.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services.roomclimatecontrol.dto; + +import javax.measure.quantity.Temperature; + +import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.types.State; + +/** + * State for {@link RoomClimateControlService} to get and set the desired temperature of a room. + * + * @author Christian Oeing - Initial contribution + */ +public class RoomClimateControlServiceState extends BoschSHCServiceState { + + private static final String TYPE = "climateControlState"; + + public RoomClimateControlServiceState() { + super(TYPE); + } + + /** + * Constructor. + * + * @param setpointTemperature Desired temperature (in degree celsius). + */ + public RoomClimateControlServiceState(double setpointTemperature) { + super(TYPE); + this.setpointTemperature = setpointTemperature; + } + + /** + * Desired temperature (in degree celsius). + * + * @apiNote Min: 5.0, Max: 30.0. + * @apiNote Can be set in 0.5 steps. + */ + private double setpointTemperature; + + /** + * Desired temperature state to set for a thing. + * + * @return Desired temperature state to set for a thing. + */ + public State getSetpointTemperatureState() { + return new QuantityType(this.setpointTemperature, SIUnits.CELSIUS); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/ShutterContactService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/ShutterContactService.java new file mode 100644 index 0000000000000..743c5031a0fad --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/ShutterContactService.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services.shuttercontact; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.services.BoschSHCService; +import org.openhab.binding.boschshc.internal.services.shuttercontact.dto.ShutterContactServiceState; + +/** + * Service to get the state of shutters. + * + * @author Christian Oeing - Initial contribution + */ +@NonNullByDefault +public class ShutterContactService extends BoschSHCService { + public ShutterContactService() { + super("ShutterContact", ShutterContactServiceState.class); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/ShutterContactState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/ShutterContactState.java new file mode 100644 index 0000000000000..9391a56f5ae4e --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/ShutterContactState.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services.shuttercontact; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Possible values for shutter contacts. + * + * @author Christian Oeing - Initial contribution + */ +@NonNullByDefault +public enum ShutterContactState { + OPEN, + CLOSED; +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/dto/ShutterContactServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/dto/ShutterContactServiceState.java new file mode 100644 index 0000000000000..7fccf982dd305 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontact/dto/ShutterContactServiceState.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services.shuttercontact.dto; + +import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState; +import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactState; + +/** + * State for the shutter contact service + * + * @author Christian Oeing - Initial contribution + */ +public class ShutterContactServiceState extends BoschSHCServiceState { + /** + * Current state of shutter contact. + */ + public ShutterContactState value; + + public ShutterContactServiceState() { + super("shutterContactState"); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/OperationState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/OperationState.java new file mode 100644 index 0000000000000..0579b0e5d86d2 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/OperationState.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services.shuttercontrol; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Operation State. + * + * @author Christian Oeing - Initial contribution + */ +@NonNullByDefault +public enum OperationState { + MOVING, + STOPPED; +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/ShutterControlService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/ShutterControlService.java new file mode 100644 index 0000000000000..ac7ebe34f958f --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/ShutterControlService.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services.shuttercontrol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.services.BoschSHCService; +import org.openhab.binding.boschshc.internal.services.shuttercontrol.dto.ShutterControlServiceState; + +/** + * Service to control the shutters of a device. + * + * @author Christian Oeing - Initial contribution + */ +@NonNullByDefault +public class ShutterControlService extends BoschSHCService { + public ShutterControlService() { + super("ShutterControl", ShutterControlServiceState.class); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/dto/ShutterControlServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/dto/ShutterControlServiceState.java new file mode 100644 index 0000000000000..c7eb5dbcfd6a3 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/shuttercontrol/dto/ShutterControlServiceState.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services.shuttercontrol.dto; + +import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState; +import org.openhab.binding.boschshc.internal.services.shuttercontrol.OperationState; + +/** + * State for a shutter control device + * + * @author Christian Oeing - Initial contribution + */ +public class ShutterControlServiceState extends BoschSHCServiceState { + /** + * Current open ratio of shutter (0.0 [closed] to 1.0 [open]) + */ + public Double level; + + /** + * Current operation state of shutter + */ + public OperationState operationState; + + public ShutterControlServiceState() { + super("shutterControlState"); + this.operationState = OperationState.STOPPED; + } + + public ShutterControlServiceState(double level) { + this(); + this.level = level; + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperaturelevel/TemperatureLevelService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperaturelevel/TemperatureLevelService.java new file mode 100644 index 0000000000000..fa66f8a7b12ed --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperaturelevel/TemperatureLevelService.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services.temperaturelevel; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.services.BoschSHCService; +import org.openhab.binding.boschshc.internal.services.temperaturelevel.dto.TemperatureLevelServiceState; + +/** + * TemperatureLevel service. + * + * @author Christian Oeing - Initial contribution + */ +@NonNullByDefault +public class TemperatureLevelService extends BoschSHCService { + public TemperatureLevelService() { + super("TemperatureLevel", TemperatureLevelServiceState.class); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperaturelevel/dto/TemperatureLevelServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperaturelevel/dto/TemperatureLevelServiceState.java new file mode 100644 index 0000000000000..415013496c410 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperaturelevel/dto/TemperatureLevelServiceState.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services.temperaturelevel.dto; + +import javax.measure.quantity.Temperature; + +import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.types.State; + +/** + * TemperatureLevel service state. + * + * @author Christian Oeing - Initial contribution + */ +public class TemperatureLevelServiceState extends BoschSHCServiceState { + + public TemperatureLevelServiceState() { + super("temperatureLevelState"); + } + + /** + * Current temperature (in degree celsius) + */ + private double temperature; + + /** + * Current temperature state to set for a thing. + * + * @return Current temperature state to use for a thing. + */ + public State getTemperatureState() { + return new QuantityType(this.temperature, SIUnits.CELSIUS); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/valvetappet/ValveTappetService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/valvetappet/ValveTappetService.java new file mode 100644 index 0000000000000..0137e48a509b8 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/valvetappet/ValveTappetService.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services.valvetappet; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.services.BoschSHCService; +import org.openhab.binding.boschshc.internal.services.valvetappet.dto.ValveTappetServiceState; + +/** + * Valve Tappet service. + * + * @author Christian Oeing - Initial contribution + */ +@NonNullByDefault +public class ValveTappetService extends BoschSHCService { + public ValveTappetService() { + super("ValveTappet", ValveTappetServiceState.class); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/valvetappet/dto/ValveTappetServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/valvetappet/dto/ValveTappetServiceState.java new file mode 100644 index 0000000000000..3c7e856e020f1 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/valvetappet/dto/ValveTappetServiceState.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services.valvetappet.dto; + +import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.types.State; + +/** + * Valve Tappet service state. + * + * @author Christian Oeing - Initial contribution + */ +public class ValveTappetServiceState extends BoschSHCServiceState { + public ValveTappetServiceState() { + super("valveTappetState"); + } + + /** + * Current open percentage of valve tappet (0 [closed] - 100 [open]). + */ + private int position; + + /** + * Current position state of valve tappet. + */ + public State getPositionState() { + return new DecimalType(this.position); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..e7346d10686b1 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + Bosch Smart Home Binding + This is the binding for Bosch Smart Home Controller. + Stefan Kästle + + diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/config/configs.xml b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/config/configs.xml new file mode 100644 index 0000000000000..0dbe7d94a305b --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/config/configs.xml @@ -0,0 +1,24 @@ + + + + + network-address + + Network address of the Bosch Smart Home Controller. + + + + password + The system password of the Bosch Smart Home Controller necessary for pairing. + + + + + + Unique ID of the device. + + + diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties new file mode 100644 index 0000000000000..e25fee3096c57 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties @@ -0,0 +1,5 @@ + +# Thing status offline descriptions +offline.conf-error-pairing = Press pairing button on the Bosch Smart Home Controller. +offline.not-reachable = Smart Home Controller is not reachable. +offline.conf-error-ssl = The SSL connection to the Bosch Smart Home Controller is not possible. diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc_de.properties b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc_de.properties new file mode 100644 index 0000000000000..0ef037c73449a --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc_de.properties @@ -0,0 +1,9 @@ +# binding +binding.boschshc.name = Bosch Smart Home Controller Binding +binding.boschshc.description = Dieses Binding integriert das Bosch Smart Home System. Durch diese können die Bosch Smart Home Geräte verwendet werden. + + +# Thing status offline descriptions +offline.conf-error-pairing = Bitte betätigen Sie den Taster am Bosch Smart Home Controller zum automatischen Verbinden. +offline.not-reachable = Smart Home Controller ist nicht erreichbar. +offline.conf-error-ssl = Die SSL Verbindung zum Bosch Smart Home Controller ist nicht möglich. diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..35f4eb5cdb1c1 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,262 @@ + + + + + + + The Bosch SHC Bridge representing the Bosch Smart Home Controller. + + + + + + + + + + + Bosch In-wall switch for light control + + + + + + + + + + + + + + + + + + Bosch TwinGuard environmental sensor + + + + + + + + + + + + + + + + + + + + + + + Bosch Contact for windows and doors + + + + + + + + + + + + + + + + Bosch Motion Detector + + + + + + + + + + + + + + + + Bosch Shutter Control + + + + + + + + + + + + + + + + Bosch Thermostat + + + + + + + + + + + + + + + + + Bosch Climate Control. This is a virtual device which is automatically created for all rooms that have + thermostats in it. + + + + + + + + + + + + Number:Temperature + + Current measured temperature. + + + + + String + + Rating of the currently measured temperature. + + + + + + + + + + + Number:Dimensionless + + Current measured humidity. + + + + + String + + Rating of current measured humidity. + + + + + + + + + + + Number:Energy + + Energy consumption of the device. + + + + + Number:Power + + Current power consumption of the device. + + + + + Number:Dimensionless + + Purity of the air. A higher value indicates a higher pollution. + + + + + String + + Overall description of the air quality. + + + + + String + + Rating of the air purity. + + + + + String + + Combined rating of the air quality. + + + + + + + + + + + Contact + + A window and door contact. + + + + + DateTime + + Timestamp of the latest motion. + + + + + Rollershutter + + Current open ratio (0 to 100). + + + + + Number:Dimensionless + + Current open ratio (0 to 100). + + + + + Number:Temperature + + Desired temperature. + + + + diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/org/openhab/binding/boschshc/internal/devices/bridge/SmartHomeControllerIssuingCA.pem b/bundles/org.openhab.binding.boschshc/src/main/resources/org/openhab/binding/boschshc/internal/devices/bridge/SmartHomeControllerIssuingCA.pem new file mode 100644 index 0000000000000..25eb08c060473 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/resources/org/openhab/binding/boschshc/internal/devices/bridge/SmartHomeControllerIssuingCA.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFETCCAvmgAwIBAgIUR8y7kFBqVMZCYZdSQWVuVJgSAqYwDQYJKoZIhvcNAQEL +BQAwYzELMAkGA1UEBhMCREUxITAfBgNVBAoMGEJvc2NoIFRoZXJtb3RlY2huaWsg +R21iSDExMC8GA1UEAwwoU21hcnQgSG9tZSBDb250cm9sbGVyIFByb2R1Y3RpdmUg +Um9vdCBDQTAeFw0xNTA4MTgwNzI0MjFaFw0yNTA4MTYwNzI0MjFaMFsxCzAJBgNV +BAYTAkRFMSEwHwYDVQQKDBhCb3NjaCBUaGVybW90ZWNobmlrIEdtYkgxKTAnBgNV +BAMMIFNtYXJ0IEhvbWUgQ29udHJvbGxlciBJc3N1aW5nIENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsBNK3PPd/E9jbf3YkZIDtfIl2Vo0Nx7oeOsh +F0L9tZwqC3+85ymB5LgFBOoHpr7tTFRb4elyPsfyv/GfXuJmDIxVAWBn/pxFzODa +J3DGJ2kvwipvMNp7IxXHhK10YsG8AaT0QaeaYGq1GRp5uNZafwAOOkrrQfwtG+za +Qn9qUxLYBrB++RN/5mk4Z7gyrq7fi84T23yMOtVkdb+mlb9qStQ3mllglqrRlJQo +MKdQxe24Farg6N3y7h5bxLJEEXGqGExDNwR46ep+4Ys7W2QeD/2LXwYvKQ+wO70+ +BNxnikkq8kPcq8694HMsfzUTBrxuHQGi6td9o+3CW01AOEvV0wIDAQABo4HEMIHB +MBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFHy1ci5zZEQaHLDAaYFYez8R +FHsXMB8GA1UdIwQYMBaAFOFQaxE4w2eoyE+f6oXGTxH1V1Y+MA4GA1UdDwEB/wQE +AwIBBjBbBgNVHR8EVDBSMFCgTqBMhkpodHRwczovLzI5Lm1jZy5lc2NyeXB0LmNv +bS9jcmw/aWQ9ZTE1MDZiMTEzOGMzNjdhOGM4NGY5ZmVhODVjNjRmMTFmNTU3NTYz +ZTANBgkqhkiG9w0BAQsFAAOCAgEAZpp9kE7Qy6tcQrfW4DJAqEcUhzg4zncJYxpb +dTn/o5TvH/uPVOfoxJgtsTFtsY/ytcPJReLcgmqrRN1gTNefdXylJr688hFyhf1Z +xGDoZG8MuzM9QXaHC6UNFzaeZj46ZYfdJiUtDXsYN82opGE6GhBju5JOLoFd2vYK +qUnVKWqdrN0KkihClry6NcfiLEA70m00pNtsVZyVGyk7DP4ErVF5K3j40T5v4ZJl +Q9ri/V97zyqXeIti8kZdla7kzJBFbGEumlUyVPRpoxdpnvWM7AgTOXXsh2sCFAA1 +0hUHVOwBZCylaNUXjKMtnA938ykhNCx+OCd2NpZBf3qB6+w2MS7dQuRvMsDJcnLq +X80QHJzXpmDsXEiwKyvmZnZbiAgoOiUSe2O6OaGsDRW8UBzi+wm42pxgbDnAcGUu +r9Cf5y0+SFS0aQkqcWbJYwPy+LQi2MJGkv34FxTOCqygluzZt+w5xZyq5PcpPNm5 +1s4Ps2+psvNhcAG3EHRF9vBnlr1MCVU04XYig54HeNGFIQQAFWFFR/9DgnH/cFLf +gPoJEZV/VZtsOjy/EsqYZMFJBzJEtKOiTCKDe+pVirDB9zrcVsJG8LGiLd7266e9 +1Eg5GjNiavG7ninMOWSJLfW4xPD6S3zxDAYjsPDJbMFqEFIF2ZvyYC1mVeflB/WM +xnZ+67w= +-----END CERTIFICATE----- diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/org/openhab/binding/boschshc/internal/devices/bridge/SmartHomeControllerProductiveRootCA.pem b/bundles/org.openhab.binding.boschshc/src/main/resources/org/openhab/binding/boschshc/internal/devices/bridge/SmartHomeControllerProductiveRootCA.pem new file mode 100644 index 0000000000000..86edc750d6450 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/resources/org/openhab/binding/boschshc/internal/devices/bridge/SmartHomeControllerProductiveRootCA.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIUIbQ+BIVcGVD29UIe+Sv6/+Qy/OUwDQYJKoZIhvcNAQEL +BQAwYzELMAkGA1UEBhMCREUxITAfBgNVBAoMGEJvc2NoIFRoZXJtb3RlY2huaWsg +R21iSDExMC8GA1UEAwwoU21hcnQgSG9tZSBDb250cm9sbGVyIFByb2R1Y3RpdmUg +Um9vdCBDQTAeFw0xNTA4MTgwNzIwMTNaFw0zNTA4MTQwNzIwMTNaMGMxCzAJBgNV +BAYTAkRFMSEwHwYDVQQKDBhCb3NjaCBUaGVybW90ZWNobmlrIEdtYkgxMTAvBgNV +BAMMKFNtYXJ0IEhvbWUgQ29udHJvbGxlciBQcm9kdWN0aXZlIFJvb3QgQ0EwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCcFmt1vu85lfXMl66Ix32tmEbc +n4bt6Oa6QIiT6zJIR2DsE85c42H8XogATWiqfp3FTbmfIIijfoj9JL6uyFkw0yrT +qfttw9KD8DRIV973F1UyAP8wPxpdt2QPJCBMmqymC6h2oT7eS6hRIMbY3SFLa5lO +4EQ10uflZnY9Yv7kTzeuEw1qWqd8kHhfDBq3k2N90oopt47ghDQ/qUmne19xp0jQ +fXFA6hfudNcU9vuZ6hvObm25++ySmRKvtuY+O/CmLVnUJngpKQWJCnYOv3/Z5StZ +5aVvLR028ozc1oqdL8fVeaJX8xIdBsSjB+gOaauEYodJzVfeLdXVb8R4CqVighci +EUuwZVhzdtA5qs2O9jLJv6JFiD+uuRn8Ip1uYiajYqkRzR2egKWFfhZvV6Yk2zuw +s8FUtagtYRwKCp+F+f+PCryLcBcnyc7iVm0Xo7kQAjzoDql4vmXQybmP6kU9qzmD +xEG02s6FHVn1X1X4htXc/+Wh0/0850T+Up2HeN+ZN92BubI8yM62mecvfx08vSb1 +5AviYkQQE37KzGeKYYbciEMeVu5sLx/lN6YIcyHY5kTUsU7SCzw7vTTsNjTzuzYa +l2fudHS8lOHaAwvZP//14cM+N9beQqLzxS7jdmFQxtToyzdbgL1OekO58fiqti4W +d88bnmMBZsl3bR9b5QIDAQABo2YwZDASBgNVHRMBAf8ECDAGAQH/AgECMB0GA1Ud +DgQWBBThUGsROMNnqMhPn+qFxk8R9VdWPjAfBgNVHSMEGDAWgBThUGsROMNnqMhP +n+qFxk8R9VdWPjAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAEp2 +bQei/KQGrnsnqugeseDVKNTOFp5o0xYz8gXEWzRCuAIo/sYKcFWziquajJWuCt/9 +CexNFWkYtV95EbunyE+wijQcnOehGSZ2gWnZiQU2fu1Y4aA5g3LlB61ljnbhX4SE +tLs31iTdjPFcWMx+rsS3+qfuOiOqQbliTykG+p/ULVLLPDCmzL/MHg3w5AiGB8k5 +i1npzDKJKpLFGFWEnECYKhPi93rLfdgmOEFalIoFB96/upm6bfOWbNvsdIspFVGe +3zSjWUvveHe9mm+VTq9aldwy/J0/81oFF7C5CmlB31sDwfY+qF5/mHKfPbrnWTIi +QAiZJxXrbmeWX9JVutRbokP1UTX63ghH+BNab/E1D020JVkimMf2Vg1/5WR2gdkN +S4j+f//uVKuCr7bPGWzcADeURlyCmW/O2CNfln+T/0YFg2lET9PAEDkZ7Js3I/4f ++Dy58LwjdQYI3Z6qKA9h0Cfgy6KOA8Omyw3QmdTAAd0EgABQ/vxNVL3Q4Oh8Eiff +ZVrpFWLgMxeRckHTMqG9SfGBdZQCO7XPz7mb/8Da6prEfw4VKvdh9llvatWeB1V1 +vqixwFVuHIWKxIiR8GXZEjIQXBmeuzdgIceYcw12HYHLUifFozaNtjxMcPcIALKz +GrR4oS2tFVZCjwF4vPAt15fsbEx/F/NfaO6SAFz8 +-----END CERTIFICATE----- diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java new file mode 100644 index 0000000000000..3bbf82a3acca0 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.bridge; + +import static org.junit.jupiter.api.Assertions.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult; +import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException; + +/** + * Tests cases for {@link BoschHttpClient}. + * + * @author Gerd Zanker - Initial contribution + */ +@NonNullByDefault +class BoschHttpClientTest { + + @Nullable + private BoschHttpClient httpClient; + + @BeforeAll + static void beforeAll() { + BoschSslUtilTest.prepareTempFolderForKeyStore(); + } + + @BeforeEach + void beforeEach() throws PairingFailedException { + SslContextFactory sslFactory = new BoschSslUtil("127.0.0.1").getSslContextFactory(); + httpClient = new BoschHttpClient("127.0.0.1", "dummy", sslFactory); + assertNotNull(httpClient); + } + + @Test + void getPairingUrl() { + assertEquals("https://127.0.0.1:8443/smarthome/clients", httpClient.getPairingUrl()); + } + + @Test + void getBoschShcUrl() { + assertEquals("https://127.0.0.1:8444/testEndpoint", httpClient.getBoschShcUrl("testEndpoint")); + } + + @Test + void getBoschSmartHomeUrl() { + assertEquals("https://127.0.0.1:8444/smarthome/endpointForTest", + httpClient.getBoschSmartHomeUrl("endpointForTest")); + } + + @Test + void getServiceUrl() { + assertEquals("https://127.0.0.1:8444/smarthome/devices/testDevice/services/testService/state", + httpClient.getServiceUrl("testService", "testDevice")); + } + + @Test + void isAccessPossible() throws InterruptedException { + assertFalse(httpClient.isAccessPossible()); + } + + @Test + void doPairing() throws InterruptedException { + assertFalse(httpClient.doPairing()); + } + + @Test + void createRequest() { + Request request = httpClient.createRequest("https://127.0.0.1", HttpMethod.GET); + assertNotNull(request); + } + + @Test + void createRequestWithObject() { + Request request = httpClient.createRequest("https://127.0.0.1", HttpMethod.GET, "someData"); + assertNotNull(request); + } + + @Test + void sendRequest() { + Request request = httpClient.createRequest("https://127.0.0.1", HttpMethod.GET); + // Null pointer exception is expected, because localhost will not answer request + assertThrows(NullPointerException.class, () -> { + httpClient.sendRequest(request, SubscribeResult.class); + }); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSslUtilTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSslUtilTest.java new file mode 100644 index 0000000000000..3da600fc8db36 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSslUtilTest.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.bridge; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.File; +import java.nio.file.Paths; +import java.security.KeyStore; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException; + +/** + * Tests cases for {@link BoschSslUtil}. + * + * @author Gerd Zanker - Initial contribution + */ +@NonNullByDefault +class BoschSslUtilTest { + + @BeforeAll + static void beforeAll() { + prepareTempFolderForKeyStore(); + } + + public static void prepareTempFolderForKeyStore() { + // Use temp folder for userdata folder + String tmpDir = System.getProperty("java.io.tmpdir"); + tmpDir = tmpDir != null ? tmpDir : "/tmp"; + System.setProperty("openhab.userdata", tmpDir); + // prepare temp folder on local drive + File tempDir = Paths.get(tmpDir, "etc").toFile(); + if (!tempDir.exists()) { + assertTrue(tempDir.mkdirs()); + } + } + + @Test + void getBoschShcClientId() { + // OpenSource Bosch SHC clients needs start with oss + assertTrue(BoschSslUtil.getBoschShcClientId().startsWith("oss")); + } + + @Test + void getBoschShcServerId() { + // OpenSource Bosch SHC clients needs start with oss + assertTrue(BoschSslUtil.getBoschShcServerId("localhost").startsWith("oss")); + assertTrue(BoschSslUtil.getBoschShcServerId("localhost").contains("localhost")); + } + + @Test + void getKeystorePath() { + BoschSslUtil sslUtil = new BoschSslUtil("123.45.67.89"); + assertTrue(sslUtil.getKeystorePath().endsWith(".jks")); + } + + /** + * Test if the keyStore can be created if it doesn't exist. + */ + @Test + void keyStoreAndFactory() throws PairingFailedException { + BoschSslUtil sslUtil1 = new BoschSslUtil("127.0.0.1"); + + // remote old, existing jks + File keyStoreFile = new File(sslUtil1.getKeystorePath()); + keyStoreFile.deleteOnExit(); + if (keyStoreFile.exists()) { + assertTrue(keyStoreFile.delete()); + } + + assertFalse(keyStoreFile.exists()); + + BoschSslUtil sslUtil2 = new BoschSslUtil("127.0.0.1"); + // fist call where keystore is created + KeyStore keyStore = sslUtil2.getKeyStoreAndCreateIfNecessary(); + assertNotNull(keyStore); + + assertTrue(keyStoreFile.exists()); + + // second call where keystore is reopened + KeyStore keyStore2 = sslUtil2.getKeyStoreAndCreateIfNecessary(); + assertNotNull(keyStore2); + + // basic test if a SSL factory instance can be created + SslContextFactory factory = sslUtil2.getSslContextFactory(); + assertNotNull(factory); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResultTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResultTest.java new file mode 100644 index 0000000000000..739ac2c785984 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResultTest.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.bridge.dto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +import com.google.gson.Gson; + +/** + * Unit tests for LongPollResult + * + * @author Christian Oeing - Initial contribution + */ +@NonNullByDefault +public class LongPollResultTest { + private final Gson gson = new Gson(); + + @Test + public void noResultsForErrorResult() { + LongPollResult longPollResult = gson.fromJson( + "{\"jsonrpc\":\"2.0\", \"error\": { \"code\":-32001, \"message\":\"No subscription with id: e8fei62b0-0\" } }", + LongPollResult.class); + assertNotEquals(null, longPollResult); + if (longPollResult != null) { + assertEquals(null, longPollResult.result); + } + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index 394835617a4c7..fcd79be957a2f 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -65,6 +65,7 @@ org.openhab.binding.bluetooth.roaming org.openhab.binding.bluetooth.ruuvitag org.openhab.binding.boschindego + org.openhab.binding.boschshc org.openhab.binding.bosesoundtouch org.openhab.binding.bsblan org.openhab.binding.bticinosmarther