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

* Initial code from create_openhab_binding_skeleton.sh

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

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

View File

@ -34,6 +34,7 @@
/bundles/org.openhab.binding.bluetooth.roaming/ @cpmeister /bundles/org.openhab.binding.bluetooth.roaming/ @cpmeister
/bundles/org.openhab.binding.bluetooth.ruuvitag/ @ssalonen /bundles/org.openhab.binding.bluetooth.ruuvitag/ @ssalonen
/bundles/org.openhab.binding.boschindego/ @jofleck /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.bosesoundtouch/ @marvkis @tratho
/bundles/org.openhab.binding.bsblan/ @hypetsch /bundles/org.openhab.binding.bsblan/ @hypetsch
/bundles/org.openhab.binding.bticinosmarther/ @MrRonfo /bundles/org.openhab.binding.bticinosmarther/ @MrRonfo

View File

@ -156,6 +156,11 @@
<artifactId>org.openhab.binding.boschindego</artifactId> <artifactId>org.openhab.binding.boschindego</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.boschshc</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.openhab.addons.bundles</groupId> <groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.bosesoundtouch</artifactId> <artifactId>org.openhab.binding.bosesoundtouch</artifactId>

View File

@ -0,0 +1,52 @@
# For Developers
## Build
To only build the Bosch SHC binding code execute
mvn -pl :org.openhab.binding.boschshc install
## Execute
After compiling a new ``org.openhab.binding.boschshc.jar``
copy it into the ``addons`` folder of your openHAB test instance.
For the first time the jar is loaded automatically as a bundle.
It should also be reloaded automatically when the jar changed.
To reload the bundle manually you need to execute:
bundle:update "openHAB Add-ons :: Bundles :: BoschSHC Binding"
or get the ID and update the bundle using the ID:
bundle:list
-> Get ID for "openHAB Add-ons :: Bundles :: BoschSHC Binding"
bundle:update <ID>
## Debugging
To get debug output and traces of the Bosch SHC binding code
add the following lines into ``userdata/etc/log4j2.xml`` Loggers XML section.
<!-- Bosch SHC for debugging -->
<Logger level="TRACE" name="org.openhab.binding.boschshc"/>
## Pairing and Certificates
We need secured and paired connection from the openHAB binding instance to the Bosch SHC.
Read more about the pairing process in [register a new client to the bosch smart home controller](https://github.com/BoschSmartHome/bosch-shc-api-docs/tree/master/postman#register-a-new-client-to-the-bosch-smart-home-controller)
A precondition for the secured connection to the Bosch SHC is a self singed key + certificate.
The key + certificate will be created and stored with the public Bosch SHC certificates in a Java Key store (jks).
The public certificates files are from https://github.com/BoschSmartHome/bosch-shc-api-docs/tree/master/best_practice.
File copies stored in ``src/main/resource``.
All three certificates and the key will be used for the HTTPS connection between
this openHAB binding and the Bosch SHC.
During pairing the openHAB binding will exchange the self singed certificate with SHC.

View File

@ -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

View File

@ -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 -> "+".

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.1.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.boschshc</artifactId>
<name>openHAB Add-ons :: Bundles :: BoschSHC Binding</name>
<dependencies>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.52</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.52</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

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

View File

@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link BoschSHCBindingConstants} class defines common constants, which
* are used across the whole binding.
*
* @author Stefan Kästle - Initial contribution
* @author Christian Oeing - added Shutter Control, ThermostatHandler
*/
@NonNullByDefault
public class BoschSHCBindingConstants {
private static final String BINDING_ID = "boschshc";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_SHC = new ThingTypeUID(BINDING_ID, "shc");
public static final ThingTypeUID THING_TYPE_INWALL_SWITCH = new ThingTypeUID(BINDING_ID, "in-wall-switch");
public static final ThingTypeUID THING_TYPE_TWINGUARD = new ThingTypeUID(BINDING_ID, "twinguard");
public static final ThingTypeUID THING_TYPE_WINDOW_CONTACT = new ThingTypeUID(BINDING_ID, "window-contact");
public static final ThingTypeUID THING_TYPE_MOTION_DETECTOR = new ThingTypeUID(BINDING_ID, "motion-detector");
public static final ThingTypeUID THING_TYPE_SHUTTER_CONTROL = new ThingTypeUID(BINDING_ID, "shutter-control");
public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat");
public static final ThingTypeUID THING_TYPE_CLIMATE_CONTROL = new ThingTypeUID(BINDING_ID, "climate-control");
// List of all Channel IDs
// Auto-generated from thing-types.xml via script, don't modify
public static final String CHANNEL_POWER_SWITCH = "power-switch";
public static final String CHANNEL_TEMPERATURE = "temperature";
public static final String CHANNEL_TEMPERATURE_RATING = "temperature-rating";
public static final String CHANNEL_HUMIDITY = "humidity";
public static final String CHANNEL_HUMIDITY_RATING = "humidity-rating";
public static final String CHANNEL_ENERGY_CONSUMPTION = "energy-consumption";
public static final String CHANNEL_POWER_CONSUMPTION = "power-consumption";
public static final String CHANNEL_PURITY = "purity";
public static final String CHANNEL_AIR_DESCRIPTION = "air-description";
public static final String CHANNEL_PURITY_RATING = "purity-rating";
public static final String CHANNEL_COMBINED_RATING = "combined-rating";
public static final String CHANNEL_CONTACT = "contact";
public static final String CHANNEL_LATEST_MOTION = "latest-motion";
public static final String CHANNEL_LEVEL = "level";
public static final String CHANNEL_VALVE_TAPPET_POSITION = "valve-tappet-position";
public static final String CHANNEL_SETPOINT_TEMPERATURE = "setpoint-temperature";
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link BoschSHCConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Stefan Kästle - Initial contribution
*/
@NonNullByDefault
public class BoschSHCConfiguration {
/**
* ID of the device as returned by the controller.
*/
public @Nullable String id;
}

View File

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

View File

@ -0,0 +1,91 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_CLIMATE_CONTROL;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_MOTION_DETECTOR;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SHC;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_THERMOSTAT;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_TWINGUARD;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.boschshc.internal.devices.bridge.BoschSHCBridgeHandler;
import org.openhab.binding.boschshc.internal.devices.climatecontrol.ClimateControlHandler;
import org.openhab.binding.boschshc.internal.devices.inwallswitch.BoschInWallSwitchHandler;
import org.openhab.binding.boschshc.internal.devices.motiondetector.MotionDetectorHandler;
import org.openhab.binding.boschshc.internal.devices.shuttercontrol.ShutterControlHandler;
import org.openhab.binding.boschshc.internal.devices.thermostat.ThermostatHandler;
import org.openhab.binding.boschshc.internal.devices.twinguard.BoschTwinguardHandler;
import org.openhab.binding.boschshc.internal.devices.windowcontact.WindowContactHandler;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
/**
* The {@link BoschSHCHandlerFactory} is responsible for creating things and
* thing handlers.
*
* @author Stefan Kästle - Initial contribution
* @author Christian Oeing - Added Shutter Control and ThermostatHandler; refactored handler mapping
*/
@NonNullByDefault
@Component(configurationPid = "binding.boschshc", service = ThingHandlerFactory.class)
public class BoschSHCHandlerFactory extends BaseThingHandlerFactory {
private static class ThingTypeHandlerMapping {
public ThingTypeUID thingTypeUID;
public Function<Thing, BaseThingHandler> handlerSupplier;
public ThingTypeHandlerMapping(ThingTypeUID thingTypeUID, Function<Thing, BaseThingHandler> handlerSupplier) {
this.thingTypeUID = thingTypeUID;
this.handlerSupplier = handlerSupplier;
}
}
private static final Collection<ThingTypeHandlerMapping> SUPPORTED_THING_TYPES = List.of(
new ThingTypeHandlerMapping(THING_TYPE_SHC, thing -> new BoschSHCBridgeHandler((Bridge) thing)),
new ThingTypeHandlerMapping(THING_TYPE_INWALL_SWITCH, BoschInWallSwitchHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_TWINGUARD, BoschTwinguardHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_WINDOW_CONTACT, WindowContactHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_MOTION_DETECTOR, MotionDetectorHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_SHUTTER_CONTROL, ShutterControlHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_THERMOSTAT, ThermostatHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_CLIMATE_CONTROL, ClimateControlHandler::new));
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES.stream().anyMatch(mapping -> mapping.thingTypeUID.equals(thingTypeUID));
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
// Search for mapping for thing type and return handler for it if found. Otherwise return null.
return SUPPORTED_THING_TYPES.stream().filter(mapping -> mapping.thingTypeUID.equals(thingTypeUID)).findFirst()
.<@Nullable BaseThingHandler> map(mapping -> mapping.handlerSupplier.apply(thing)).orElse(null);
}
}

View File

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

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.bridge;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link BoschSHCBridgeConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Stefan Kästle - Initial contribution
*/
@NonNullByDefault
public class BoschSHCBridgeConfiguration {
/**
* IP address of the Bosch Smart Home Controller
*/
public String ipAddress = "";
/**
* Password of the Bosch Smart Home Controller. Set during initialization via the Bosch app.
*/
public String password = "";
}

View File

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

View File

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

View File

@ -0,0 +1,62 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.bridge;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Payload as POST data for triggering a RPC call on the Bosch Smart Home Controller.
*
* @author Stefan Kästle - Initial contribution
*/
@NonNullByDefault
class JsonRpcRequest {
public String jsonrpc;
public String method;
public String[] params;
public JsonRpcRequest(String jsonrpc, String method, String[] params) {
this.jsonrpc = jsonrpc;
this.method = method;
this.params = params;
}
public JsonRpcRequest() {
this("", "", new String[0]);
}
public String getJsonrpc() {
return jsonrpc;
}
public void setJsonrpc(String jsonrpc) {
this.jsonrpc = jsonrpc;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String[] getParams() {
return params;
}
public void setParams(String[] params) {
this.params = params;
}
}

View File

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

View File

@ -0,0 +1,57 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.bridge.dto;
import java.util.List;
import com.google.gson.annotations.SerializedName;
/**
* Represents a single devices connected to the Bosch Smart Home Controller.
*
* Example from Json:
*
* {
* "@type":"device",
* "rootDeviceId":"64-da-a0-02-14-9b",
* "id":"hdm:HomeMaticIP:3014F711A00004953859F31B",
* "deviceServiceIds":["PowerMeter","PowerSwitch","PowerSwitchProgram","Routing"],
* "manufacturer":"BOSCH",
* "roomId":"hz_3",
* "deviceModel":"PSM",
* "serial":"3014F711A00004953859F31B",
* "profile":"GENERIC",
* "name":"Coffee Machine",
* "status":"AVAILABLE",
* "childDeviceIds":[]
* }
*
* @author Stefan Kästle - Initial contribution
*/
public class Device {
@SerializedName("@type")
public String type;
public String rootDeviceId;
public String id;
public List<String> deviceSerivceIDs;
public String manufacturer;
public String roomId;
public String deviceModel;
public String serial;
public String profile;
public String name;
public String status;
public List<String> childDeviceIds;
}

View File

@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.bridge.dto;
import com.google.gson.JsonElement;
import com.google.gson.annotations.SerializedName;
/**
* Represents a device status update as represented by the Smart Home
* Controller.
*
* @author Stefan Kästle - Initial contribution
* @author Christian Oeing - refactorings of e.g. server registration
*/
public class DeviceStatusUpdate {
/**
* Url path of the service the update came from.
*/
public String path;
/**
* The type of message.
*/
@SerializedName("@type")
public String type;
/**
* Name of service the update came from.
*/
public String id;
/**
* Current state of device. Serialized as JSON.
*/
public JsonElement state;
/**
* Id of device the update is for.
*/
public String deviceId;
@Override
public String toString() {
return this.deviceId + "state: " + this.type;
}
}

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.bridge.dto;
/**
* Error response of the Controller for a Long Poll API call.
*
* @author Stefan Kästle - Initial contribution
*/
public class LongPollError {
public static final int SUBSCRIPTION_INVALID = -32001;
/**
* {
* "jsonrpc":"2.0",
* "error": {
* "code":-32001,
* "message":"No subscription with id: e8fei62b0-0"
* }
* }
*/
public class ErrorInfo {
public int code;
public String message;
}
public String jsonrpc;
public ErrorInfo error;
}

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.bridge.dto;
import java.util.ArrayList;
/**
* Response of the Controller for a Long Poll API call.
*
* @author Stefan Kästle - Initial contribution
*/
public class LongPollResult {
/**
* {"result":[
* ..{
* ...."path":"/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch",
* ...."@type":"DeviceServiceData",
* ...."id":"PowerSwitch",
* ...."state":{
* ......"@type":"powerSwitchState",
* ......"switchState":"ON"
* ....},
* ...."deviceId":"hdm:HomeMaticIP:3014F711A0001916D859A8A9"}
* ],"jsonrpc":"2.0"}
*/
public ArrayList<DeviceStatusUpdate> result;
public String jsonrpc;
}

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.bridge.dto;
import com.google.gson.annotations.SerializedName;
/**
* A room as represented by the controller.
*
* Json example:
* {"@type":"room","id":"hz_1","iconId":"icon_room_bedroom","name":"Bedroom"}
*
* @author Stefan Kästle - Initial contribution
*/
public class Room {
@SerializedName("@type")
public String type;
public String id;
public String name;
}

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.bridge.dto;
/**
* Response of the Controller for a Long Poll API call.
*
* The result field will contain the subscription ID needed for further API calls (e.g. the long polling call)
*
* @author Stefan Kästle - Initial contribution
*/
public class SubscribeResult {
private String result;
private String jsonrpc;
public String getResult() {
return this.result;
}
public String getJsonrpc() {
return this.jsonrpc;
}
}

View File

@ -0,0 +1,106 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.climatecontrol;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_SETPOINT_TEMPERATURE;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.roomclimatecontrol.RoomClimateControlService;
import org.openhab.binding.boschshc.internal.services.roomclimatecontrol.dto.RoomClimateControlServiceState;
import org.openhab.binding.boschshc.internal.services.temperaturelevel.TemperatureLevelService;
import org.openhab.binding.boschshc.internal.services.temperaturelevel.dto.TemperatureLevelServiceState;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
/**
* A virtual device which controls up to six Bosch Smart Home radiator thermostats in a room.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public final class ClimateControlHandler extends BoschSHCHandler {
private RoomClimateControlService roomClimateControlService;
/**
* Constructor.
*
* @param thing The Bosch Smart Home device that should be handled.
*/
public ClimateControlHandler(Thing thing) {
super(thing);
this.roomClimateControlService = new RoomClimateControlService();
}
@Override
protected void initializeServices() throws BoschSHCException {
super.createService(TemperatureLevelService::new, this::updateChannels, List.of(CHANNEL_TEMPERATURE));
super.registerService(this.roomClimateControlService, this::updateChannels,
List.of(CHANNEL_SETPOINT_TEMPERATURE));
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
switch (channelUID.getId()) {
case CHANNEL_SETPOINT_TEMPERATURE:
if (command instanceof QuantityType<?>) {
updateSetpointTemperature((QuantityType<?>) command);
}
break;
}
}
/**
* Updates the channels which are linked to the {@link TemperatureLevelService} of the device.
*
* @param state Current state of {@link TemperatureLevelService}.
*/
private void updateChannels(TemperatureLevelServiceState state) {
super.updateState(CHANNEL_TEMPERATURE, state.getTemperatureState());
}
/**
* Updates the channels which are linked to the {@link RoomClimateControlService} of the device.
*
* @param state Current state of {@link RoomClimateControlService}.
*/
private void updateChannels(RoomClimateControlServiceState state) {
super.updateState(CHANNEL_SETPOINT_TEMPERATURE, state.getSetpointTemperatureState());
}
/**
* Sets the desired temperature for the device.
*
* @param quantityType Command which contains the new desired temperature.
*/
private void updateSetpointTemperature(QuantityType<?> quantityType) {
QuantityType<?> celsiusType = quantityType.toUnit(SIUnits.CELSIUS);
if (celsiusType == null) {
logger.debug("Could not convert quantity command to celsius");
return;
}
double setpointTemperature = celsiusType.doubleValue();
this.updateServiceState(this.roomClimateControlService,
new RoomClimateControlServiceState(setpointTemperature));
}
}

View File

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

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.inwallswitch.dto;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
/**
* PowerMeterState
*
* @author Stefan Kästle - Initial contribution
*/
public class PowerMeterState extends BoschSHCServiceState {
public PowerMeterState() {
super("powerMeterState");
}
public double energyConsumption;
public double powerConsumption;
}

View File

@ -0,0 +1,72 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.motiondetector;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_LATEST_MOTION;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.devices.motiondetector.dto.LatestMotionState;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import com.google.gson.JsonElement;
/**
* MotionDetectorHandler
*
* @author Stefan Kästle - Initial contribution
*/
@NonNullByDefault
public class MotionDetectorHandler extends BoschSHCHandler {
public MotionDetectorHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Handle command for: {} - {}", channelUID.getThingUID(), command);
if (CHANNEL_LATEST_MOTION.equals(channelUID.getId())) {
if (command instanceof RefreshType) {
LatestMotionState state = this.getState("LatestMotion", LatestMotionState.class);
if (state != null) {
updateLatestMotionState(state);
}
}
}
}
void updateLatestMotionState(LatestMotionState state) {
DateTimeType date = new DateTimeType(state.latestMotionDetected);
updateState(CHANNEL_LATEST_MOTION, date);
}
@Override
public void processUpdate(String id, JsonElement state) {
logger.debug("Motion detector: received update: {} {}", id, state);
@Nullable
LatestMotionState latestMotionState = GSON.fromJson(state, LatestMotionState.class);
if (latestMotionState == null) {
logger.warn("Received unknown update in in-wall switch: {}", state);
return;
}
updateLatestMotionState(latestMotionState);
}
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.motiondetector.dto;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
/**
* {
* "result": [
* {
* "path": "/devices/hdm:ZigBee:000d6f0004b95a62/services/LatestMotion",
* "@type": "DeviceServiceData",
* "id": "LatestMotion",
* "state": {
* "latestMotionDetected": "2020-04-03T19:02:19.054Z",
* "@type": "latestMotionState"
* },
* "deviceId": "hdm:ZigBee:000d6f0004b95a62"
* }
* ],
* "jsonrpc": "2.0"
* }
*
* @author Stefan Kästle - Initial contribution
*/
public class LatestMotionState extends BoschSHCServiceState {
public LatestMotionState() {
super("latestMotionState");
}
public String latestMotionDetected;
}

View File

@ -0,0 +1,106 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.shuttercontrol;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_LEVEL;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.shuttercontrol.OperationState;
import org.openhab.binding.boschshc.internal.services.shuttercontrol.ShutterControlService;
import org.openhab.binding.boschshc.internal.services.shuttercontrol.dto.ShutterControlServiceState;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StopMoveType;
import org.openhab.core.library.types.UpDownType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
/**
* Handler for a shutter control device
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public class ShutterControlHandler extends BoschSHCHandler {
/**
* Utility functions to convert data between Bosch things and openHAB items
*/
static final class DataConversion {
public static int levelToOpenPercentage(double level) {
return (int) Math.round((1 - level) * 100);
}
public static double openPercentageToLevel(double openPercentage) {
return (100 - openPercentage) / 100.0;
}
}
private ShutterControlService shutterControlService;
public ShutterControlHandler(Thing thing) {
super(thing);
this.shutterControlService = new ShutterControlService();
}
@Override
protected void initializeServices() throws BoschSHCException {
super.initializeServices();
this.registerService(this.shutterControlService, this::updateChannels, List.of(CHANNEL_LEVEL));
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
if (command instanceof UpDownType) {
// Set full close/open as target state
UpDownType upDownType = (UpDownType) command;
ShutterControlServiceState state = new ShutterControlServiceState();
if (upDownType == UpDownType.UP) {
state.level = 1.0;
} else if (upDownType == UpDownType.DOWN) {
state.level = 0.0;
} else {
logger.warn("Received unknown UpDownType command: {}", upDownType);
return;
}
this.updateServiceState(this.shutterControlService, state);
} else if (command instanceof StopMoveType) {
StopMoveType stopMoveType = (StopMoveType) command;
if (stopMoveType == StopMoveType.STOP) {
// Set STOPPED operation state
ShutterControlServiceState state = new ShutterControlServiceState();
state.operationState = OperationState.STOPPED;
this.updateServiceState(this.shutterControlService, state);
}
} else if (command instanceof PercentType) {
// Set specific level
PercentType percentType = (PercentType) command;
double level = DataConversion.openPercentageToLevel(percentType.doubleValue());
this.updateServiceState(this.shutterControlService, new ShutterControlServiceState(level));
}
}
private void updateChannels(ShutterControlServiceState state) {
if (state.level != null) {
// Convert level to open ratio
int openPercentage = DataConversion.levelToOpenPercentage(state.level);
updateState(CHANNEL_LEVEL, new PercentType(openPercentage));
}
}
}

View File

@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.thermostat;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_VALVE_TAPPET_POSITION;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.temperaturelevel.TemperatureLevelService;
import org.openhab.binding.boschshc.internal.services.temperaturelevel.dto.TemperatureLevelServiceState;
import org.openhab.binding.boschshc.internal.services.valvetappet.ValveTappetService;
import org.openhab.binding.boschshc.internal.services.valvetappet.dto.ValveTappetServiceState;
import org.openhab.core.thing.Thing;
/**
* Handler for a thermostat device.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public final class ThermostatHandler extends BoschSHCHandler {
public ThermostatHandler(Thing thing) {
super(thing);
}
@Override
protected void initializeServices() throws BoschSHCException {
this.createService(TemperatureLevelService::new, this::updateChannels, List.of(CHANNEL_TEMPERATURE));
this.createService(ValveTappetService::new, this::updateChannels, List.of(CHANNEL_VALVE_TAPPET_POSITION));
}
/**
* Updates the channels which are linked to the {@link TemperatureLevelService} of the device.
*
* @param state Current state of {@link TemperatureLevelService}.
*/
private void updateChannels(TemperatureLevelServiceState state) {
super.updateState(CHANNEL_TEMPERATURE, state.getTemperatureState());
}
/**
* Updates the channels which are linked to the {@link ValveTappetService} of the device.
*
* @param state Current state of {@link ValveTappetService}.
*/
private void updateChannels(ValveTappetServiceState state) {
super.updateState(CHANNEL_VALVE_TAPPET_POSITION, state.getPositionState());
}
}

View File

@ -0,0 +1,92 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.twinguard;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*;
import javax.measure.quantity.Dimensionless;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.devices.twinguard.dto.AirQualityLevelState;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import com.google.gson.JsonElement;
import com.google.gson.JsonSyntaxException;
/**
* The {@link BoschSHCHandler} is responsible for handling commands for the TwinGuard handler.
*
* @author Stefan Kästle - Initial contribution
*/
@NonNullByDefault
public class BoschTwinguardHandler extends BoschSHCHandler {
public BoschTwinguardHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
Bridge bridge = this.getBridge();
if (bridge != null) {
logger.debug("Handle command for: {} - {}", channelUID.getThingUID(), command);
if (command instanceof RefreshType) {
AirQualityLevelState state = this.getState("AirQualityLevel", AirQualityLevelState.class);
if (state != null) {
updateAirQualityState(state);
}
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Bridge is NUL");
}
}
void updateAirQualityState(AirQualityLevelState state) {
updateState(CHANNEL_TEMPERATURE, new QuantityType<Temperature>(state.temperature, SIUnits.CELSIUS));
updateState(CHANNEL_TEMPERATURE_RATING, new StringType(state.temperatureRating));
updateState(CHANNEL_HUMIDITY, new QuantityType<Dimensionless>(state.humidity, Units.ONE));
updateState(CHANNEL_HUMIDITY_RATING, new StringType(state.humidityRating));
updateState(CHANNEL_PURITY, new QuantityType<Dimensionless>(state.purity, Units.ONE));
updateState(CHANNEL_AIR_DESCRIPTION, new StringType(state.description));
updateState(CHANNEL_PURITY_RATING, new StringType(state.purityRating));
updateState(CHANNEL_COMBINED_RATING, new StringType(state.combinedRating));
}
@Override
public void processUpdate(String id, JsonElement state) throws JsonSyntaxException {
logger.debug("Twinguard: received update: {} {}", id, state);
AirQualityLevelState parsed = GSON.fromJson(state, AirQualityLevelState.class);
if (parsed == null) {
logger.warn("Received unknown update in in-wall switch: {}", state);
return;
}
logger.debug("Parsed switch state of {}: {}", this.getBoschID(), parsed);
updateAirQualityState(parsed);
}
}

View File

@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.twinguard.dto;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
/**
* Represents the state of a device as reported from the Smart Home Controller
*
* @author Stefan Kästle - Initial contribution
*/
public class AirQualityLevelState extends BoschSHCServiceState {
public AirQualityLevelState() {
super("airQualityLevelState");
}
/*
* {"maxTemperature":25,"minTemperature":20,"custom":false,"name":"HALLWAY","maxHumidity":60,"minHumidity":40,
* "maxPurity":1000}
*/
class ComfortZone {
double maxTemperature;
double minTemperature;
boolean custom;
String name;
double maxHumidity;
double minHumidity;
double maxPurity;
}
/**
* {"temperatureRating":"GOOD","humidityRating":"MEDIUM","purity":620,"comfortZone":....,"@type":"airQualityLevelState",
* "purityRating":"GOOD","temperature":23.77,"description":"LITTLE_DRY","humidity":32.69,"combinedRating":"MEDIUM"}
*/
public String temperatureRating;
public String humidityRating;
public int purity;
public ComfortZone comfortZone;
public String purityRating;
public double temperature;
public String description;
public double humidity;
public String combinedRating;
}

View File

@ -0,0 +1,50 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.windowcontact;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_CONTACT;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactService;
import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactState;
import org.openhab.binding.boschshc.internal.services.shuttercontact.dto.ShutterContactServiceState;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.State;
/**
* The {@link BoschSHCHandler} is responsible for handling Bosch window/door contacts.
*
* @author Stefan Kästle - Initial contribution
*/
@NonNullByDefault
public class WindowContactHandler extends BoschSHCHandler {
public WindowContactHandler(Thing thing) {
super(thing);
}
@Override
protected void initializeServices() throws BoschSHCException {
this.createService(ShutterContactService::new, this::updateChannels, List.of(CHANNEL_CONTACT));
}
private void updateChannels(ShutterContactServiceState state) {
State contact = state.value == ShutterContactState.CLOSED ? OpenClosedType.CLOSED : OpenClosedType.OPEN;
updateState(CHANNEL_CONTACT, contact);
}
}

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.exceptions;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception class for Bosch Smart Home controller errors.
*
* @author Gerd Zanker - Initial contribution
*/
@SuppressWarnings("serial")
@NonNullByDefault
public class BoschSHCException extends Exception {
public BoschSHCException() {
}
public BoschSHCException(String message) {
super(message);
}
public BoschSHCException(String message, Throwable e) {
super(message, e);
}
}

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.exceptions;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Thrown if the long polling failed
*
* @author Christian Oeing - Initial contribution
*/
@SuppressWarnings("serial")
@NonNullByDefault
public class LongPollingFailedException extends BoschSHCException {
public LongPollingFailedException(String message, Throwable e) {
super(message, e);
}
}

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.exceptions;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Thrown if the pairing failed multiple times
*
* @author Gerd Zanker - Initial contribution
*/
@SuppressWarnings("serial")
@NonNullByDefault
public class PairingFailedException extends BoschSHCException {
public PairingFailedException() {
}
public PairingFailedException(String message) {
super(message);
}
public PairingFailedException(String message, Throwable cause) {
super(message, cause);
}
}

View File

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

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.services.dto;
import com.google.gson.annotations.SerializedName;
/**
* Base Bosch Smart Home Controller service state.
*
* @author Christian Oeing - Initial contribution
*/
public class BoschSHCServiceState {
@SerializedName("@type")
private final String type;
protected BoschSHCServiceState(String type) {
this.type = type;
}
public String getType() {
return type;
}
}

View File

@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.services.dto;
/**
* Generic error response of the Bosch REST API.
*
* @author Christian Oeing - Initial contribution
*/
public class JsonRestExceptionResponse extends BoschSHCServiceState {
public JsonRestExceptionResponse() {
super("JsonRestExceptionResponseEntity");
this.errorCode = "";
this.statusCode = 0;
}
/**
* The error code of the occurred Exception.
*/
public String errorCode;
/**
* The HTTP status of the error.
*/
public int statusCode;
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.services.powerswitch;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.services.BoschSHCService;
import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState;
/**
* Service to get and set the state of a power switch.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public class PowerSwitchService extends BoschSHCService<PowerSwitchServiceState> {
public PowerSwitchService() {
super("PowerSwitch", PowerSwitchServiceState.class);
}
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.services.powerswitch;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Possible states of a power switch.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public enum PowerSwitchState {
ON,
OFF
}

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.services.powerswitch.dto;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState;
/**
* Represents the state of a power switch device as reported from the Smart Home Controller
*
* @author Stefan Kästle - Initial contribution
* @author Christian Oeing - Adjustments to match general service state structure
*/
public class PowerSwitchServiceState extends BoschSHCServiceState {
public PowerSwitchServiceState() {
super("powerSwitchState");
}
/**
* Current state of power switch.
*/
public PowerSwitchState switchState;
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.services.roomclimatecontrol;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.services.BoschSHCService;
import org.openhab.binding.boschshc.internal.services.roomclimatecontrol.dto.RoomClimateControlServiceState;
/**
* Service of a virtual device which controls the radiator thermostats in a room.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public class RoomClimateControlService extends BoschSHCService<RoomClimateControlServiceState> {
public RoomClimateControlService() {
super("RoomClimateControl", RoomClimateControlServiceState.class);
}
}

View File

@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.services.roomclimatecontrol.dto;
import javax.measure.quantity.Temperature;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.types.State;
/**
* State for {@link RoomClimateControlService} to get and set the desired temperature of a room.
*
* @author Christian Oeing - Initial contribution
*/
public class RoomClimateControlServiceState extends BoschSHCServiceState {
private static final String TYPE = "climateControlState";
public RoomClimateControlServiceState() {
super(TYPE);
}
/**
* Constructor.
*
* @param setpointTemperature Desired temperature (in degree celsius).
*/
public RoomClimateControlServiceState(double setpointTemperature) {
super(TYPE);
this.setpointTemperature = setpointTemperature;
}
/**
* Desired temperature (in degree celsius).
*
* @apiNote Min: 5.0, Max: 30.0.
* @apiNote Can be set in 0.5 steps.
*/
private double setpointTemperature;
/**
* Desired temperature state to set for a thing.
*
* @return Desired temperature state to set for a thing.
*/
public State getSetpointTemperatureState() {
return new QuantityType<Temperature>(this.setpointTemperature, SIUnits.CELSIUS);
}
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.services.shuttercontact;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.services.BoschSHCService;
import org.openhab.binding.boschshc.internal.services.shuttercontact.dto.ShutterContactServiceState;
/**
* Service to get the state of shutters.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public class ShutterContactService extends BoschSHCService<ShutterContactServiceState> {
public ShutterContactService() {
super("ShutterContact", ShutterContactServiceState.class);
}
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.services.shuttercontact;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Possible values for shutter contacts.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public enum ShutterContactState {
OPEN,
CLOSED;
}

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.services.shuttercontact.dto;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactState;
/**
* State for the shutter contact service
*
* @author Christian Oeing - Initial contribution
*/
public class ShutterContactServiceState extends BoschSHCServiceState {
/**
* Current state of shutter contact.
*/
public ShutterContactState value;
public ShutterContactServiceState() {
super("shutterContactState");
}
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.services.shuttercontrol;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Operation State.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public enum OperationState {
MOVING,
STOPPED;
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.services.shuttercontrol;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.services.BoschSHCService;
import org.openhab.binding.boschshc.internal.services.shuttercontrol.dto.ShutterControlServiceState;
/**
* Service to control the shutters of a device.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public class ShutterControlService extends BoschSHCService<ShutterControlServiceState> {
public ShutterControlService() {
super("ShutterControl", ShutterControlServiceState.class);
}
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.services.shuttercontrol.dto;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
import org.openhab.binding.boschshc.internal.services.shuttercontrol.OperationState;
/**
* State for a shutter control device
*
* @author Christian Oeing - Initial contribution
*/
public class ShutterControlServiceState extends BoschSHCServiceState {
/**
* Current open ratio of shutter (0.0 [closed] to 1.0 [open])
*/
public Double level;
/**
* Current operation state of shutter
*/
public OperationState operationState;
public ShutterControlServiceState() {
super("shutterControlState");
this.operationState = OperationState.STOPPED;
}
public ShutterControlServiceState(double level) {
this();
this.level = level;
}
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.services.temperaturelevel;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.services.BoschSHCService;
import org.openhab.binding.boschshc.internal.services.temperaturelevel.dto.TemperatureLevelServiceState;
/**
* TemperatureLevel service.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public class TemperatureLevelService extends BoschSHCService<TemperatureLevelServiceState> {
public TemperatureLevelService() {
super("TemperatureLevel", TemperatureLevelServiceState.class);
}
}

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.services.temperaturelevel.dto;
import javax.measure.quantity.Temperature;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.types.State;
/**
* TemperatureLevel service state.
*
* @author Christian Oeing - Initial contribution
*/
public class TemperatureLevelServiceState extends BoschSHCServiceState {
public TemperatureLevelServiceState() {
super("temperatureLevelState");
}
/**
* Current temperature (in degree celsius)
*/
private double temperature;
/**
* Current temperature state to set for a thing.
*
* @return Current temperature state to use for a thing.
*/
public State getTemperatureState() {
return new QuantityType<Temperature>(this.temperature, SIUnits.CELSIUS);
}
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.services.valvetappet;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.services.BoschSHCService;
import org.openhab.binding.boschshc.internal.services.valvetappet.dto.ValveTappetServiceState;
/**
* Valve Tappet service.
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public class ValveTappetService extends BoschSHCService<ValveTappetServiceState> {
public ValveTappetService() {
super("ValveTappet", ValveTappetServiceState.class);
}
}

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.services.valvetappet.dto;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.types.State;
/**
* Valve Tappet service state.
*
* @author Christian Oeing - Initial contribution
*/
public class ValveTappetServiceState extends BoschSHCServiceState {
public ValveTappetServiceState() {
super("valveTappetState");
}
/**
* Current open percentage of valve tappet (0 [closed] - 100 [open]).
*/
private int position;
/**
* Current position state of valve tappet.
*/
public State getPositionState() {
return new DecimalType(this.position);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,103 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.bridge;
import static org.junit.jupiter.api.Assertions.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult;
import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException;
/**
* Tests cases for {@link BoschHttpClient}.
*
* @author Gerd Zanker - Initial contribution
*/
@NonNullByDefault
class BoschHttpClientTest {
@Nullable
private BoschHttpClient httpClient;
@BeforeAll
static void beforeAll() {
BoschSslUtilTest.prepareTempFolderForKeyStore();
}
@BeforeEach
void beforeEach() throws PairingFailedException {
SslContextFactory sslFactory = new BoschSslUtil("127.0.0.1").getSslContextFactory();
httpClient = new BoschHttpClient("127.0.0.1", "dummy", sslFactory);
assertNotNull(httpClient);
}
@Test
void getPairingUrl() {
assertEquals("https://127.0.0.1:8443/smarthome/clients", httpClient.getPairingUrl());
}
@Test
void getBoschShcUrl() {
assertEquals("https://127.0.0.1:8444/testEndpoint", httpClient.getBoschShcUrl("testEndpoint"));
}
@Test
void getBoschSmartHomeUrl() {
assertEquals("https://127.0.0.1:8444/smarthome/endpointForTest",
httpClient.getBoschSmartHomeUrl("endpointForTest"));
}
@Test
void getServiceUrl() {
assertEquals("https://127.0.0.1:8444/smarthome/devices/testDevice/services/testService/state",
httpClient.getServiceUrl("testService", "testDevice"));
}
@Test
void isAccessPossible() throws InterruptedException {
assertFalse(httpClient.isAccessPossible());
}
@Test
void doPairing() throws InterruptedException {
assertFalse(httpClient.doPairing());
}
@Test
void createRequest() {
Request request = httpClient.createRequest("https://127.0.0.1", HttpMethod.GET);
assertNotNull(request);
}
@Test
void createRequestWithObject() {
Request request = httpClient.createRequest("https://127.0.0.1", HttpMethod.GET, "someData");
assertNotNull(request);
}
@Test
void sendRequest() {
Request request = httpClient.createRequest("https://127.0.0.1", HttpMethod.GET);
// Null pointer exception is expected, because localhost will not answer request
assertThrows(NullPointerException.class, () -> {
httpClient.sendRequest(request, SubscribeResult.class);
});
}
}

View File

@ -0,0 +1,102 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.bridge;
import static org.junit.jupiter.api.Assertions.*;
import java.io.File;
import java.nio.file.Paths;
import java.security.KeyStore;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException;
/**
* Tests cases for {@link BoschSslUtil}.
*
* @author Gerd Zanker - Initial contribution
*/
@NonNullByDefault
class BoschSslUtilTest {
@BeforeAll
static void beforeAll() {
prepareTempFolderForKeyStore();
}
public static void prepareTempFolderForKeyStore() {
// Use temp folder for userdata folder
String tmpDir = System.getProperty("java.io.tmpdir");
tmpDir = tmpDir != null ? tmpDir : "/tmp";
System.setProperty("openhab.userdata", tmpDir);
// prepare temp folder on local drive
File tempDir = Paths.get(tmpDir, "etc").toFile();
if (!tempDir.exists()) {
assertTrue(tempDir.mkdirs());
}
}
@Test
void getBoschShcClientId() {
// OpenSource Bosch SHC clients needs start with oss
assertTrue(BoschSslUtil.getBoschShcClientId().startsWith("oss"));
}
@Test
void getBoschShcServerId() {
// OpenSource Bosch SHC clients needs start with oss
assertTrue(BoschSslUtil.getBoschShcServerId("localhost").startsWith("oss"));
assertTrue(BoschSslUtil.getBoschShcServerId("localhost").contains("localhost"));
}
@Test
void getKeystorePath() {
BoschSslUtil sslUtil = new BoschSslUtil("123.45.67.89");
assertTrue(sslUtil.getKeystorePath().endsWith(".jks"));
}
/**
* Test if the keyStore can be created if it doesn't exist.
*/
@Test
void keyStoreAndFactory() throws PairingFailedException {
BoschSslUtil sslUtil1 = new BoschSslUtil("127.0.0.1");
// remote old, existing jks
File keyStoreFile = new File(sslUtil1.getKeystorePath());
keyStoreFile.deleteOnExit();
if (keyStoreFile.exists()) {
assertTrue(keyStoreFile.delete());
}
assertFalse(keyStoreFile.exists());
BoschSslUtil sslUtil2 = new BoschSslUtil("127.0.0.1");
// fist call where keystore is created
KeyStore keyStore = sslUtil2.getKeyStoreAndCreateIfNecessary();
assertNotNull(keyStore);
assertTrue(keyStoreFile.exists());
// second call where keystore is reopened
KeyStore keyStore2 = sslUtil2.getKeyStoreAndCreateIfNecessary();
assertNotNull(keyStore2);
// basic test if a SSL factory instance can be created
SslContextFactory factory = sslUtil2.getSslContextFactory();
assertNotNull(factory);
}
}

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.bridge.dto;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import com.google.gson.Gson;
/**
* Unit tests for LongPollResult
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public class LongPollResultTest {
private final Gson gson = new Gson();
@Test
public void noResultsForErrorResult() {
LongPollResult longPollResult = gson.fromJson(
"{\"jsonrpc\":\"2.0\", \"error\": { \"code\":-32001, \"message\":\"No subscription with id: e8fei62b0-0\" } }",
LongPollResult.class);
assertNotEquals(null, longPollResult);
if (longPollResult != null) {
assertEquals(null, longPollResult.result);
}
}
}

View File

@ -65,6 +65,7 @@
<module>org.openhab.binding.bluetooth.roaming</module> <module>org.openhab.binding.bluetooth.roaming</module>
<module>org.openhab.binding.bluetooth.ruuvitag</module> <module>org.openhab.binding.bluetooth.ruuvitag</module>
<module>org.openhab.binding.boschindego</module> <module>org.openhab.binding.boschindego</module>
<module>org.openhab.binding.boschshc</module>
<module>org.openhab.binding.bosesoundtouch</module> <module>org.openhab.binding.bosesoundtouch</module>
<module>org.openhab.binding.bsblan</module> <module>org.openhab.binding.bsblan</module>
<module>org.openhab.binding.bticinosmarther</module> <module>org.openhab.binding.bticinosmarther</module>