Binding for the Bosch Smart Home.
- [Bosch Smart Home Binding](#bosch-smart-home-binding)
+ - [Changelog](#changelog)
- [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)
+ - [In-Wall switches & Smart Plugs](#in-wall-switches--smart-plugs)
+ - [TwinGuard smoke detector](#twinguard-smoke-detector)
+ - [Door/Window contact](#doorwindow-contact)
+ - [Motion Detector](#motion-detector)
+ - [Shutter Control](#shutter-control)
+ - [Thermostat](#thermostat)
+ - [Climate Control](#climate-control)
+ - [Wall Thermostat](#wall-thermostat)
- [Limitations](#limitations)
- [Discovery](#discovery)
- [Bridge Configuration](#bridge-configuration)
## Supported Things
-### Bosch In-Wall switches & Bosch Smart Plugs
+### In-Wall switches & Smart Plugs
+
+A simple light control.
**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. |
+| Channel Type ID | Item Type | Writable | 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. |
+
+### TwinGuard smoke detector
-### Bosch TwinGuard smoke detector
+The Twinguard smoke detector warns you in case of fire and constantly monitors the air.
**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. |
+| Channel Type ID | Item Type | Writable | Description |
+| ------------------ | -------------------- | :------: | ------------------------------------------------------------------------------------------------- |
+| temperature | Number:Temperature | ☐ | Current measured temperature. |
+| temperature-rating | String | ☐ | Rating of the currently measured temperature. |
+| humidity | Number:Dimensionless | ☐ | Current measured humidity (0 to 100). |
+| 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
+### Door/Window contact
+
+Detects open windows and doors.
**Thing Type ID**: `window-contact`
-| Channel Type ID | Item Type | Description |
-|-----------------|-----------|------------------------------|
-| contact | Contact | Contact state of the device. |
+| Channel Type ID | Item Type | Writable | Description |
+| --------------- | --------- | :------: | ---------------------------- |
+| contact | Contact | ☐ | Contact state of the device. |
+
+### Motion Detector
-### Bosch Motion Detector
+Detects every movement through an intelligent combination of passive infra-red technology and an additional temperature sensor.
**Thing Type ID**: `motion-detector`
-| Channel Type ID | Item Type | Description |
-|-----------------|-----------|--------------------------------|
-| latest-motion | DateTime | The date of the latest motion. |
+| Channel Type ID | Item Type | Writable | Description |
+| --------------- | --------- | :------: | ------------------------------ |
+| latest-motion | DateTime | ☐ | The date of the latest motion. |
-### Bosch Shutter Control in-wall
+### Shutter Control
+
+Control of your shutter to take any position you desire.
**Thing Type ID**: `shutter-control`
-| Channel Type ID | Item Type | Description |
-|-----------------|---------------|------------------------------------------|
-| level | Rollershutter | Current open ratio (0 to 100, Step 0.5). |
+| Channel Type ID | Item Type | Writable | Description |
+| --------------- | ------------- | :------: | ---------------------------------------- |
+| level | Rollershutter | ☑ | Current open ratio (0 to 100, Step 0.5). |
+
+### Thermostat
-### Bosch Thermostat
+Radiator 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). |
+| Channel Type ID | Item Type | Writable | Description |
+| --------------------- | -------------------- | :------: | ---------------------------------------------- |
+| temperature | Number:Temperature | ☐ | Current measured temperature. |
+| valve-tappet-position | Number:Dimensionless | ☐ | Current open ratio of valve tappet (0 to 100). |
+| child-lock | Switch | ☑ | Indicates if child lock is active. |
+
+### Climate Control
-### Bosch Climate Control
+A virtual device which controls up to six Bosch Smart Home radiator thermostats in a room.
**Thing Type ID**: `climate-control`
-| Channel Type ID | Item Type | Description |
-|----------------------|--------------------|-------------------------------|
-| temperature | Number:Temperature | Current measured temperature. |
-| setpoint-temperature | Number:Temperature | Desired temperature. |
+| Channel Type ID | Item Type | Writable | Description |
+| -------------------- | ------------------ | :------: | ----------------------------- |
+| temperature | Number:Temperature | ☐ | Current measured temperature. |
+| setpoint-temperature | Number:Temperature | ☑ | Desired temperature. |
+
+### Wall Thermostat
+
+Display of the current room temperature as well as the relative humidity in the room.
+
+**Thing Type ID**: `wall-thermostat`
+
+| Channel Type ID | Item Type | Writable | Description |
+| --------------- | -------------------- | :------: | ------------------------------------- |
+| temperature | Number:Temperature | ☐ | Current measured temperature. |
+| humidity | Number:Dimensionless | ☐ | Current measured humidity (0 to 100). |
## Limitations
Bosch IDs for found devices are displayed in the openHAB log on bootup (`OPENHAB_FOLDER/userdata/logs/openhab.log`)
The log can also be called using the following command.
+
```
tail -f /var/log/openhab/openhab.log /var/log/openhab/events.log
```
+
Alternatively, the log can be viewed using the OpenHab Log Viewer (frontail) via http://openhab:9001.
Example:
*
* @author Stefan Kästle - Initial contribution
* @author Christian Oeing - added Shutter Control, ThermostatHandler
+ * @author Christian Oeing - Added WallThermostatHandler
*/
@NonNullByDefault
public class BoschSHCBindingConstants {
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");
+ public static final ThingTypeUID THING_TYPE_WALL_THERMOSTAT = new ThingTypeUID(BINDING_ID, "wall-thermostat");
// List of all Channel IDs
// Auto-generated from thing-types.xml via script, don't modify
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";
+ public static final String CHANNEL_CHILD_LOCK = "child-lock";
}
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.bridge.BridgeHandler;
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;
*/
@Override
public void initialize() {
- this.config = getConfigAs(BoschSHCConfiguration.class);
+ var config = this.config = getConfigAs(BoschSHCConfiguration.class);
+ String deviceId = config.id;
+ if (deviceId == null || deviceId.isEmpty()) {
+ this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "@text/offline.conf-error.empty-device-id");
+ return;
+ }
+
+ // Try to get device info to make sure the device exists
try {
- this.initializeServices();
+ var bridgeHandler = this.getBridgeHandler();
+ var info = bridgeHandler.getDeviceInfo(deviceId);
+ logger.trace("Device initialized:\n{}", info.toString());
+ } catch (InterruptedException | TimeoutException | ExecutionException | BoschSHCException e) {
+ this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+ return;
+ }
- // Mark immediately as online - if the bridge is online, the thing is too.
- this.updateStatus(ThingStatus.ONLINE);
+ // Initialize device services
+ try {
+ this.initializeServices();
} catch (BoschSHCException e) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+ return;
}
+
+ this.updateStatus(ThingStatus.ONLINE);
}
/**
// 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 (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()));
- } catch (InterruptedException e) {
- this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
- String.format("Interrupted refresh state from service %s: %s",
- deviceService.service.getServiceName(), e.getMessage()));
- Thread.currentThread().interrupt();
- }
+ this.refreshServiceState(deviceService.service);
}
}
}
* @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 {
+ protected BridgeHandler getBridgeHandler() throws BoschSHCException {
Bridge bridge = this.getBridge();
if (bridge == null) {
- throw new BoschSHCException(String.format("No valid bridge set for %s", this.getThing()));
+ throw new BoschSHCException(String.format("No valid bridge set for %s (%s)", this.getThing().getLabel(),
+ this.getThing().getUID().getAsString()));
}
- BoschSHCBridgeHandler bridgeHandler = (BoschSHCBridgeHandler) bridge.getHandler();
+ BridgeHandler bridgeHandler = (BridgeHandler) bridge.getHandler();
if (bridgeHandler == null) {
- throw new BoschSHCException(String.format("Bridge of %s has no valid bridge handler", this.getThing()));
+ throw new BoschSHCException(String.format("Bridge of %s (%s) has no valid bridge handler",
+ this.getThing().getLabel(), this.getThing().getUID().getAsString()));
}
return bridgeHandler;
}
return null;
}
try {
- BoschSHCBridgeHandler bridgeHandler = this.getBridgeHandler();
+ BridgeHandler bridgeHandler = this.getBridgeHandler();
return bridgeHandler.getState(deviceId, stateName, classOfT);
} catch (TimeoutException | ExecutionException | BoschSHCException e) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void registerService(
TService service, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels)
throws BoschSHCException {
- BoschSHCBridgeHandler bridgeHandler = this.getBridgeHandler();
+ BridgeHandler bridgeHandler = this.getBridgeHandler();
String deviceId = this.getBoschID();
if (deviceId == null) {
}
}
+ /**
+ * Lets a service handle a received command.
+ * Sets the status of the device to offline if handling the command fails.
+ *
+ * @param <TService> Type of service.
+ * @param <TState> Type of service state.
+ * @param service Service which should handle command.
+ * @param command Command to handle.
+ */
+ protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void handleServiceCommand(
+ TService service, Command command) {
+ try {
+ if (command instanceof RefreshType) {
+ this.refreshServiceState(service);
+ } else {
+ TState state = service.handleCommand(command);
+ this.updateServiceState(service, state);
+ }
+ } catch (BoschSHCException e) {
+ this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+ String.format("Error when service %s should handle command %s: %s", service.getServiceName(),
+ command.getClass().getName(), e.getMessage()));
+ }
+ }
+
+ /**
+ * Requests a service to refresh its state.
+ * Sets the device offline if request fails.
+ *
+ * @param <TService> Type of service.
+ * @param <TState> Type of service state.
+ * @param service Service to refresh state for.
+ */
+ private <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void refreshServiceState(
+ TService service) {
+ try {
+ service.refreshState();
+ } catch (TimeoutException | ExecutionException | BoschSHCException e) {
+ this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+ String.format("Error when trying to refresh state from service %s: %s", service.getServiceName(),
+ e.getMessage()));
+ } catch (InterruptedException e) {
+ this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String
+ .format("Interrupted refresh state from service %s: %s", service.getServiceName(), e.getMessage()));
+ Thread.currentThread().interrupt();
+ }
+ }
+
/**
* Registers a service of this device.
*
*/
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 static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*;
import java.util.Collection;
import java.util.List;
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.bridge.BridgeHandler;
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.lightcontrol.LightControlHandler;
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.twinguard.TwinguardHandler;
+import org.openhab.binding.boschshc.internal.devices.wallthermostat.WallThermostatHandler;
import org.openhab.binding.boschshc.internal.devices.windowcontact.WindowContactHandler;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
*
* @author Stefan Kästle - Initial contribution
* @author Christian Oeing - Added Shutter Control and ThermostatHandler; refactored handler mapping
+ * @author Christian Oeing - Added WallThermostatHandler
*/
@NonNullByDefault
@Component(configurationPid = "binding.boschshc", service = ThingHandlerFactory.class)
}
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_SHC, thing -> new BridgeHandler((Bridge) thing)),
+ new ThingTypeHandlerMapping(THING_TYPE_INWALL_SWITCH, LightControlHandler::new),
+ new ThingTypeHandlerMapping(THING_TYPE_TWINGUARD, TwinguardHandler::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));
+ new ThingTypeHandlerMapping(THING_TYPE_CLIMATE_CONTROL, ClimateControlHandler::new),
+ new ThingTypeHandlerMapping(THING_TYPE_WALL_THERMOSTAT, WallThermostatHandler::new));
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import java.util.function.BiFunction;
+import java.util.function.Predicate;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
if (content != null) {
String body = GSON.toJson(content);
- logger.trace("create request for {} and content {}", url, body);
+ logger.trace("create request for {} and content {}", url, content.toString());
request = request.content(new StringContentProvider(body));
} else {
logger.trace("create request for {}", url);
*
* @param request Request to send
* @param responseContentClass Type of expected response
+ * @param contentValidator Checks if the parsed response is valid
+ * @param errorResponseHandler Optional ustom error response handling. If not provided a generic exception is thrown
* @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
+ * @throws BoschSHCException in case of a custom handled error response
*/
- public <TContent> TContent sendRequest(Request request, Class<TContent> responseContentClass)
- throws InterruptedException, TimeoutException, ExecutionException {
+ public <TContent> TContent sendRequest(Request request, Class<TContent> responseContentClass,
+ Predicate<TContent> contentValidator,
+ @Nullable BiFunction<Integer, String, BoschSHCException> errorResponseHandler)
+ throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
logger.trace("Send request: {}", request.toString());
ContentResponse contentResponse = request.send();
- logger.debug("Received response: {} - status: {}", contentResponse.getContentAsString(),
- contentResponse.getStatus());
+ String textContent = contentResponse.getContentAsString();
+
+ Integer statusCode = contentResponse.getStatus();
+ if (!HttpStatus.getCode(statusCode).isSuccess()) {
+ if (errorResponseHandler != null) {
+ throw errorResponseHandler.apply(statusCode, textContent);
+ } else {
+ throw new ExecutionException(String.format("Request failed with status code %s", statusCode), null);
+ }
+ }
+
+ logger.debug("Received response: {} - status: {}", textContent, statusCode);
try {
@Nullable
- TContent content = GSON.fromJson(contentResponse.getContentAsString(), responseContentClass);
+ TContent content = GSON.fromJson(textContent, responseContentClass);
if (content == null) {
throw new ExecutionException(String.format("Received no content in response, expected type %s",
responseContentClass.getName()), null);
}
+ if (!contentValidator.test(content)) {
+ throw new ExecutionException(String.format("Received invalid content for type %s: %s",
+ responseContentClass.getName(), content), null);
+ }
return content;
} catch (JsonSyntaxException e) {
throw new ExecutionException(String.format("Received invalid content in response, expected type %s: %s",
+++ /dev/null
-/**
- * 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 = "";
-}
+++ /dev/null
-/**
- * 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.*;
-
-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.http.HttpStatus;
-import org.eclipse.jetty.util.ssl.SslContextFactory;
-import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
-import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
-import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceStatusUpdate;
-import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
-import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room;
-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.osgi.framework.FrameworkUtil;
-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() {
- logger.debug("Initialize {} Version {}", FrameworkUtil.getBundle(getClass()).getSymbolicName(),
- FrameworkUtil.getBundle(getClass()).getVersion());
-
- // Read configuration
- BoschSHCBridgeConfiguration config = getConfigAs(BoschSHCBridgeConfiguration.class);
-
- String ipAddress = config.ipAddress.trim();
- if (ipAddress.isEmpty()) {
- this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
- "@text/offline.conf-error-empty-ip");
- return;
- }
-
- String password = config.password.trim();
- if (password.isEmpty()) {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
- "@text/offline.conf-error-empty-password");
- return;
- }
-
- SslContextFactory factory;
- try {
- // prepare SSL key and certificates
- factory = new BoschSslUtil(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(ipAddress, 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;
- }
-
- // general checks are OK, therefore set the status to unknown and wait for initial access
- this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE);
-
- // Initialize bridge in the background.
- // Start initial access the first time
- scheduleInitialAccess(httpClient);
- }
-
- @Override
- public void dispose() {
- // Cancel scheduled pairing.
- @Nullable
- ScheduledFuture<?> scheduledPairing = this.scheduledPairing;
- if (scheduledPairing != null) {
- scheduledPairing.cancel(true);
- this.scheduledPairing = null;
- }
-
- // Stop long polling.
- this.longPolling.stop();
-
- @Nullable
- 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: {}", this, httpClient);
-
- try {
- // check if SCH is offline
- if (!httpClient.isOnline()) {
- // update status already if access is not possible
- this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
- "@text/offline.conf-error-offline");
- // restart later initial access
- scheduleInitialAccess(httpClient);
- return;
- }
-
- // SHC is online
- // check if SHC access is not possible and pairing necessary
- if (!httpClient.isAccessPossible()) {
- // update status description to show pairing test
- 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);
- return;
- }
-
- // SHC is online and access is possible
- // print rooms and devices
- boolean thingReachable = true;
- thingReachable &= this.getRooms();
- thingReachable &= this.getDevices();
- if (!thingReachable) {
- this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
- "@text/offline.not-reachable");
- // restart initial access
- scheduleInitialAccess(httpClient);
- return;
- }
-
- // start long polling loop
- this.updateStatus(ThingStatus.ONLINE);
- try {
- this.longPolling.start(httpClient);
- } catch (LongPollingFailedException e) {
- this.handleLongPollFailure(e);
- }
-
- } catch (InterruptedException e) {
- this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, "@text/offline.interrupted");
- Thread.currentThread().interrupt();
- }
- }
-
- /**
- * Get a list of connected devices from the Smart-Home Controller
- *
- * @throws InterruptedException in case bridge is stopped
- */
- private boolean getDevices() throws InterruptedException {
- @Nullable
- BoschHttpClient httpClient = this.httpClient;
- if (httpClient == null) {
- return false;
- }
-
- try {
- logger.debug("Sending http request to Bosch to request devices: {}", httpClient);
- String url = httpClient.getBoschSmartHomeUrl("devices");
- ContentResponse contentResponse = httpClient.createRequest(url, GET).send();
-
- // check HTTP status code
- if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
- logger.debug("Request devices failed with status code: {}", contentResponse.getStatus());
- return false;
- }
-
- String content = contentResponse.getContentAsString();
- logger.debug("Request devices completed with success: {} - status 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.warn("Request devices failed because of {}!", e.getMessage());
- return false;
- }
-
- return true;
- }
-
- /**
- * Bridge callback handler for the results of long polls.
- *
- * It will check the result and
- * forward the received to the bosch thing handlers.
- *
- * @param result Results from Long Polling
- */
- 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
- @Nullable
- ThingHandler baseHandler = childThing.getHandler();
- if (baseHandler != null && baseHandler instanceof BoschSHCHandler) {
- BoschSHCHandler handler = (BoschSHCHandler) baseHandler;
- @Nullable
- 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);
- }
- }
- }
- }
-
- /**
- * Bridge callback handler for the failures during long polls.
- *
- * It will update the bridge status and try to access the SHC again.
- *
- * @param e error during long polling
- */
- private void handleLongPollFailure(Throwable e) {
- logger.warn("Long polling failed, will try to reconnect", e);
- @Nullable
- BoschHttpClient httpClient = this.httpClient;
- if (httpClient == null) {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
- "@text/offline.long-polling-failed.http-client-null");
- return;
- }
-
- this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
- "@text/offline.long-polling-failed.trying-to-reconnect");
- scheduleInitialAccess(httpClient);
- }
-
- /**
- * Get a list of rooms from the Smart-Home controller
- *
- * @throws InterruptedException in case bridge is stopped
- */
- private boolean getRooms() throws InterruptedException {
- @Nullable
- 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();
-
- // check HTTP status code
- if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
- logger.debug("Request rooms failed with status code: {}", contentResponse.getStatus());
- return false;
- }
-
- String content = contentResponse.getContentAsString();
- logger.debug("Request rooms completed with success: {} - status 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("Request rooms failed because of {}!", 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 {
- @Nullable
- 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 {
- @Nullable
- 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
- return request.send();
- }
-}
--- /dev/null
+/**
+ * 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 BridgeConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Stefan Kästle - Initial contribution
+ */
+@NonNullByDefault
+public class BridgeConfiguration {
+
+ /**
+ * 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 = "";
+}
--- /dev/null
+/**
+ * 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.*;
+
+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.http.HttpStatus;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceStatusUpdate;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room;
+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.osgi.framework.FrameworkUtil;
+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 BridgeHandler extends BaseBridgeHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(BridgeHandler.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 BridgeHandler(Bridge bridge) {
+ super(bridge);
+
+ this.longPolling = new LongPolling(this.scheduler, this::handleLongPollResult, this::handleLongPollFailure);
+ }
+
+ @Override
+ public void initialize() {
+ logger.debug("Initialize {} Version {}", FrameworkUtil.getBundle(getClass()).getSymbolicName(),
+ FrameworkUtil.getBundle(getClass()).getVersion());
+
+ // Read configuration
+ BridgeConfiguration config = getConfigAs(BridgeConfiguration.class);
+
+ String ipAddress = config.ipAddress.trim();
+ if (ipAddress.isEmpty()) {
+ this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "@text/offline.conf-error-empty-ip");
+ return;
+ }
+
+ String password = config.password.trim();
+ if (password.isEmpty()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "@text/offline.conf-error-empty-password");
+ return;
+ }
+
+ SslContextFactory factory;
+ try {
+ // prepare SSL key and certificates
+ factory = new BoschSslUtil(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(ipAddress, 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;
+ }
+
+ // general checks are OK, therefore set the status to unknown and wait for initial access
+ this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE);
+
+ // Initialize bridge in the background.
+ // Start initial access the first time
+ scheduleInitialAccess(httpClient);
+ }
+
+ @Override
+ public void dispose() {
+ // Cancel scheduled pairing.
+ @Nullable
+ ScheduledFuture<?> scheduledPairing = this.scheduledPairing;
+ if (scheduledPairing != null) {
+ scheduledPairing.cancel(true);
+ this.scheduledPairing = null;
+ }
+
+ // Stop long polling.
+ this.longPolling.stop();
+
+ @Nullable
+ 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: {}", this, httpClient);
+
+ try {
+ // check if SCH is offline
+ if (!httpClient.isOnline()) {
+ // update status already if access is not possible
+ this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
+ "@text/offline.conf-error-offline");
+ // restart later initial access
+ scheduleInitialAccess(httpClient);
+ return;
+ }
+
+ // SHC is online
+ // check if SHC access is not possible and pairing necessary
+ if (!httpClient.isAccessPossible()) {
+ // update status description to show pairing test
+ 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);
+ return;
+ }
+
+ // SHC is online and access is possible
+ // print rooms and devices
+ boolean thingReachable = true;
+ thingReachable &= this.getRooms();
+ thingReachable &= this.getDevices();
+ if (!thingReachable) {
+ this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+ "@text/offline.not-reachable");
+ // restart initial access
+ scheduleInitialAccess(httpClient);
+ return;
+ }
+
+ // start long polling loop
+ this.updateStatus(ThingStatus.ONLINE);
+ try {
+ this.longPolling.start(httpClient);
+ } catch (LongPollingFailedException e) {
+ this.handleLongPollFailure(e);
+ }
+
+ } catch (InterruptedException e) {
+ this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, "@text/offline.interrupted");
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * Get a list of connected devices from the Smart-Home Controller
+ *
+ * @throws InterruptedException in case bridge is stopped
+ */
+ private boolean getDevices() throws InterruptedException {
+ @Nullable
+ BoschHttpClient httpClient = this.httpClient;
+ if (httpClient == null) {
+ return false;
+ }
+
+ try {
+ logger.debug("Sending http request to Bosch to request devices: {}", httpClient);
+ String url = httpClient.getBoschSmartHomeUrl("devices");
+ ContentResponse contentResponse = httpClient.createRequest(url, GET).send();
+
+ // check HTTP status code
+ if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
+ logger.debug("Request devices failed with status code: {}", contentResponse.getStatus());
+ return false;
+ }
+
+ String content = contentResponse.getContentAsString();
+ logger.debug("Request devices completed with success: {} - status 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.deviceServiceIds != null) {
+ for (String s : d.deviceServiceIds) {
+ logger.info(".... service: {}", s);
+ }
+ }
+ }
+ }
+ } catch (TimeoutException | ExecutionException e) {
+ logger.warn("Request devices failed because of {}!", e.getMessage());
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Bridge callback handler for the results of long polls.
+ *
+ * It will check the result and
+ * forward the received to the bosch thing handlers.
+ *
+ * @param result Results from Long Polling
+ */
+ private void handleLongPollResult(LongPollResult result) {
+ for (DeviceStatusUpdate update : result.result) {
+ if (update != null && update.state != null) {
+ logger.debug("Got update of type {}: {}", update.type, update.state);
+
+ var updateDeviceId = update.deviceId;
+ if (updateDeviceId == null) {
+ continue;
+ }
+
+ logger.debug("Got update for {}", updateDeviceId);
+
+ boolean handled = false;
+
+ Bridge bridge = this.getThing();
+ for (Thing childThing : bridge.getThings()) {
+ // All children of this should implement BoschSHCHandler
+ @Nullable
+ ThingHandler baseHandler = childThing.getHandler();
+ if (baseHandler != null && baseHandler instanceof BoschSHCHandler) {
+ BoschSHCHandler handler = (BoschSHCHandler) baseHandler;
+ @Nullable
+ String deviceId = handler.getBoschID();
+
+ handled = true;
+ logger.debug("Registered device: {} - looking for {}", deviceId, updateDeviceId);
+
+ if (deviceId != null && updateDeviceId.equals(deviceId)) {
+ logger.debug("Found child: {} - calling processUpdate (id: {}) with {}", handler, update.id,
+ 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: {}", updateDeviceId);
+ }
+ }
+ }
+ }
+
+ /**
+ * Bridge callback handler for the failures during long polls.
+ *
+ * It will update the bridge status and try to access the SHC again.
+ *
+ * @param e error during long polling
+ */
+ private void handleLongPollFailure(Throwable e) {
+ logger.warn("Long polling failed, will try to reconnect", e);
+ @Nullable
+ BoschHttpClient httpClient = this.httpClient;
+ if (httpClient == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
+ "@text/offline.long-polling-failed.http-client-null");
+ return;
+ }
+
+ this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
+ "@text/offline.long-polling-failed.trying-to-reconnect");
+ scheduleInitialAccess(httpClient);
+ }
+
+ /**
+ * Get a list of rooms from the Smart-Home controller
+ *
+ * @throws InterruptedException in case bridge is stopped
+ */
+ private boolean getRooms() throws InterruptedException {
+ @Nullable
+ 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();
+
+ // check HTTP status code
+ if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
+ logger.debug("Request rooms failed with status code: {}", contentResponse.getStatus());
+ return false;
+ }
+
+ String content = contentResponse.getContentAsString();
+ logger.debug("Request rooms completed with success: {} - status 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("Request rooms failed because of {}!", e.getMessage());
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ public Device getDeviceInfo(String deviceId)
+ throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
+ @Nullable
+ BoschHttpClient httpClient = this.httpClient;
+ if (httpClient == null) {
+ throw new BoschSHCException("HTTP client not initialized");
+ }
+
+ String url = httpClient.getBoschSmartHomeUrl(String.format("devices/%s", deviceId));
+ Request request = httpClient.createRequest(url, GET);
+
+ return httpClient.sendRequest(request, Device.class, Device::isValid, (Integer statusCode, String content) -> {
+ JsonRestExceptionResponse errorResponse = gson.fromJson(content, JsonRestExceptionResponse.class);
+ if (errorResponse != null && JsonRestExceptionResponse.isValid(errorResponse)) {
+ if (errorResponse.errorCode.equals(JsonRestExceptionResponse.ENTITY_NOT_FOUND)) {
+ return new BoschSHCException("@text/offline.conf-error.invalid-device-id");
+ } else {
+ return new BoschSHCException(
+ String.format("Request for info of device %s failed with status code %d and error code %s",
+ deviceId, errorResponse.statusCode, errorResponse.errorCode));
+ }
+ } else {
+ return new BoschSHCException(String.format("Request for info for device %s failed with status code %d",
+ deviceId, statusCode));
+ }
+ });
+ }
+
+ /**
+ * 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 {
+ @Nullable
+ 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 = BoschSHCServiceState.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 {
+ @Nullable
+ 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
+ return request.send();
+ }
+}
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);
+ logger.debug("Subscribe: Sending request: {} - using httpClient {}", request.toString(),
+ httpClient.toString());
Request httpRequest = httpClient.createRequest(url, POST, request);
- SubscribeResult response = httpClient.sendRequest(httpRequest, SubscribeResult.class);
+ SubscribeResult response = httpClient.sendRequest(httpRequest, SubscribeResult.class,
+ SubscribeResult::isValid, null);
logger.debug("Subscribe: Got subscription ID: {} {}", response.getResult(), response.getJsonrpc());
String subscriptionId = response.getResult();
return subscriptionId;
- } catch (TimeoutException | ExecutionException e) {
+ } catch (TimeoutException | ExecutionException | BoschSHCException e) {
throw new LongPollingFailedException(
String.format("Error on subscribe (Http client: %s): %s", httpClient.toString(), e.getMessage()),
e);
public String rootDeviceId;
public String id;
- public List<String> deviceSerivceIDs;
+ public List<String> deviceServiceIds;
public String manufacturer;
public String roomId;
public String deviceModel;
public String name;
public String status;
public List<String> childDeviceIds;
+
+ public static Boolean isValid(Device obj) {
+ return obj != null && obj.id != null;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "Type %s; RootDeviceId: %s; Id: %s; Device Service Ids: %s; Manufacturer: %s; Room Id: %s; Device Model: %s; Serial: %s; Profile: %s; Name: %s; Status: %s; Child Device Ids: %s ",
+ this.type, this.rootDeviceId, this.id,
+ this.deviceServiceIds != null ? String.join(", ", this.deviceServiceIds) : "null", this.manufacturer,
+ this.roomId, this.deviceModel, this.serial, this.profile, this.name, this.status,
+ this.childDeviceIds != null ? String.join(", ", this.childDeviceIds) : "null");
+ }
}
*/
package org.openhab.binding.boschshc.internal.devices.bridge.dto;
+import org.eclipse.jdt.annotation.Nullable;
+
import com.google.gson.JsonElement;
import com.google.gson.annotations.SerializedName;
/**
* Id of device the update is for.
*/
- public String deviceId;
+ public @Nullable String deviceId;
@Override
public String toString() {
public String getJsonrpc() {
return this.jsonrpc;
}
+
+ public static Boolean isValid(SubscribeResult obj) {
+ return obj != null && obj.result != null && obj.jsonrpc != null;
+ }
}
+++ /dev/null
-/**
- * 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);
- }
- }
- }
-}
+++ /dev/null
-/**
- * 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;
-}
--- /dev/null
+/**
+ * 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.lightcontrol;
+
+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.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.services.powermeter.PowerMeterService;
+import org.openhab.binding.boschshc.internal.services.powermeter.dto.PowerMeterServiceState;
+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.State;
+
+/**
+ * A simple light control.
+ *
+ * @author Stefan Kästle - Initial contribution
+ */
+@NonNullByDefault
+public class LightControlHandler extends BoschSHCHandler {
+
+ private final PowerSwitchService powerSwitchService;
+
+ public LightControlHandler(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));
+ this.createService(PowerMeterService::new, this::updateChannels,
+ List.of(CHANNEL_POWER_CONSUMPTION, CHANNEL_ENERGY_CONSUMPTION));
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ super.handleCommand(channelUID, command);
+
+ switch (channelUID.getId()) {
+ case CHANNEL_POWER_SWITCH:
+ if (command instanceof OnOffType) {
+ updatePowerSwitchState((OnOffType) command);
+ }
+ break;
+ }
+ }
+
+ /**
+ * Updates the channels which are linked to the {@link PowerMeterService} of the device.
+ *
+ * @param state Current state of {@link PowerMeterService}.
+ */
+ private void updateChannels(PowerMeterServiceState state) {
+ super.updateState(CHANNEL_POWER_CONSUMPTION, new QuantityType<Power>(state.powerConsumption, Units.WATT));
+ super.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);
+ }
+}
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_LATEST_MOTION;
+import java.util.List;
+
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.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.services.latestmotion.LatestMotionService;
+import org.openhab.binding.boschshc.internal.services.latestmotion.dto.LatestMotionServiceState;
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
+ * Detects every movement through an intelligent combination of passive infra-red technology and an additional
+ * temperature sensor.
*
* @author Stefan Kästle - Initial contribution
+ * @author Christian Oeing - Use service instead of custom logic
*/
@NonNullByDefault
public class MotionDetectorHandler extends BoschSHCHandler {
}
@Override
- public void handleCommand(ChannelUID channelUID, Command command) {
- logger.debug("Handle command for: {} - {}", channelUID.getThingUID(), command);
+ protected void initializeServices() throws BoschSHCException {
+ super.initializeServices();
- if (CHANNEL_LATEST_MOTION.equals(channelUID.getId())) {
- if (command instanceof RefreshType) {
- LatestMotionState state = this.getState("LatestMotion", LatestMotionState.class);
- if (state != null) {
- updateLatestMotionState(state);
- }
- }
- }
+ this.createService(LatestMotionService::new, this::updateChannels, List.of(CHANNEL_LATEST_MOTION));
}
- void updateLatestMotionState(LatestMotionState state) {
+ private void updateChannels(LatestMotionServiceState 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);
- }
}
+++ /dev/null
-/**
- * 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;
-}
import org.openhab.core.types.Command;
/**
- * Handler for a shutter control device
+ * Control of your shutter to take any position you desire.
*
* @author Christian Oeing - Initial contribution
*/
*/
package org.openhab.binding.boschshc.internal.devices.thermostat;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_CHILD_LOCK;
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 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.childlock.ChildLockService;
+import org.openhab.binding.boschshc.internal.services.childlock.dto.ChildLockServiceState;
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.ChannelUID;
import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
/**
* Handler for a thermostat device.
@NonNullByDefault
public final class ThermostatHandler extends BoschSHCHandler {
+ private ChildLockService childLockService;
+
public ThermostatHandler(Thing thing) {
super(thing);
+ this.childLockService = new ChildLockService();
}
@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));
+ this.registerService(this.childLockService, this::updateChannels, List.of(CHANNEL_CHILD_LOCK));
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ super.handleCommand(channelUID, command);
+
+ switch (channelUID.getId()) {
+ case CHANNEL_CHILD_LOCK:
+ this.handleServiceCommand(this.childLockService, command);
+ break;
+ }
}
/**
- * Updates the channels which are linked to the {@link TemperatureLevelService} of the device.
+ * Updates the channels which are linked to the {@link TemperatureLevelService}
+ * of the device.
*
* @param state Current state of {@link TemperatureLevelService}.
*/
}
/**
- * Updates the channels which are linked to the {@link ValveTappetService} of the device.
+ * 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());
}
+
+ /**
+ * Updates the channels which are linked to the {@link ChildLockService} of the
+ * device.
+ *
+ * @param state Current state of {@link ChildLockService}.
+ */
+ private void updateChannels(ChildLockServiceState state) {
+ super.updateState(CHANNEL_CHILD_LOCK, state.getActiveState());
+ }
}
+++ /dev/null
-/**
- * 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.PERCENT));
- updateState(CHANNEL_HUMIDITY_RATING, new StringType(state.humidityRating));
- updateState(CHANNEL_PURITY, new QuantityType<Dimensionless>(state.purity, Units.PARTS_PER_MILLION));
- 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);
- }
-}
--- /dev/null
+/**
+ * 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.CHANNEL_AIR_DESCRIPTION;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_COMBINED_RATING;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_HUMIDITY;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_HUMIDITY_RATING;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_PURITY;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_PURITY_RATING;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE_RATING;
+
+import java.util.List;
+
+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.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.services.airqualitylevel.AirQualityLevelService;
+import org.openhab.binding.boschshc.internal.services.airqualitylevel.dto.AirQualityLevelServiceState;
+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.Thing;
+
+/**
+ * The Twinguard smoke detector warns you in case of fire and constantly monitors the air.
+ *
+ * @author Stefan Kästle - Initial contribution
+ * @author Christian Oeing - Use service instead of custom logic
+ */
+@NonNullByDefault
+public class TwinguardHandler extends BoschSHCHandler {
+
+ public TwinguardHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ protected void initializeServices() throws BoschSHCException {
+ super.initializeServices();
+
+ this.createService(AirQualityLevelService::new, this::updateChannels,
+ List.of(CHANNEL_TEMPERATURE, CHANNEL_TEMPERATURE_RATING, CHANNEL_HUMIDITY, CHANNEL_HUMIDITY_RATING,
+ CHANNEL_PURITY, CHANNEL_PURITY_RATING, CHANNEL_AIR_DESCRIPTION, CHANNEL_COMBINED_RATING));
+ }
+
+ private void updateChannels(AirQualityLevelServiceState 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.PERCENT));
+ updateState(CHANNEL_HUMIDITY_RATING, new StringType(state.humidityRating));
+ updateState(CHANNEL_PURITY, new QuantityType<Dimensionless>(state.purity, Units.PARTS_PER_MILLION));
+ updateState(CHANNEL_PURITY_RATING, new StringType(state.purityRating));
+ updateState(CHANNEL_AIR_DESCRIPTION, new StringType(state.description));
+ updateState(CHANNEL_COMBINED_RATING, new StringType(state.combinedRating));
+ }
+}
+++ /dev/null
-/**
- * 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;
-}
--- /dev/null
+/**
+ * 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.wallthermostat;
+
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_HUMIDITY;
+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.humiditylevel.HumidityLevelService;
+import org.openhab.binding.boschshc.internal.services.humiditylevel.dto.HumidityLevelServiceState;
+import org.openhab.binding.boschshc.internal.services.temperaturelevel.TemperatureLevelService;
+import org.openhab.binding.boschshc.internal.services.temperaturelevel.dto.TemperatureLevelServiceState;
+import org.openhab.core.thing.Thing;
+
+/**
+ * Handler for a wall thermostat device.
+ *
+ * @author Christian Oeing - Initial contribution
+ */
+@NonNullByDefault
+public final class WallThermostatHandler extends BoschSHCHandler {
+
+ public WallThermostatHandler(Thing thing) {
+ super(thing);
+ }
+
+ @Override
+ protected void initializeServices() throws BoschSHCException {
+ this.createService(TemperatureLevelService::new, this::updateChannels, List.of(CHANNEL_TEMPERATURE));
+ this.createService(HumidityLevelService::new, this::updateChannels, List.of(CHANNEL_HUMIDITY));
+ }
+
+ /**
+ * 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 HumidityLevelService} of the device.
+ *
+ * @param state Current state of {@link HumidityLevelService}.
+ */
+ private void updateChannels(HumidityLevelServiceState state) {
+ super.updateState(CHANNEL_HUMIDITY, state.getHumidityState());
+ }
+}
import org.openhab.core.types.State;
/**
- * The {@link BoschSHCHandler} is responsible for handling Bosch window/door contacts.
+ * Detects open windows and doors.
*
* @author Stefan Kästle - Initial contribution
*/
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.bridge.BridgeHandler;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
+import org.openhab.core.types.Command;
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/
+ * 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);
+ protected final Logger logger = LoggerFactory.getLogger(BoschSHCService.class);
/**
* Unique service name
*/
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;
+ private @Nullable BridgeHandler bridgeHandler;
/**
* Id of device the service belongs to
* Constructor
*
* @param serviceName Unique name of the service.
- * @param stateClass State class that this service uses for data transfers from/to the device.
+ * @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;
*
* @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.
+ * @param stateUpdateListener Function to call when a state update was received
+ * from the device.
*/
- public void initialize(BoschSHCBridgeHandler bridgeHandler, String deviceId,
+ public void initialize(BridgeHandler bridgeHandler, String deviceId,
@Nullable Consumer<TState> stateUpdateListener) {
this.bridgeHandler = bridgeHandler;
this.deviceId = deviceId;
if (deviceId == null) {
return null;
}
- BoschSHCBridgeHandler bridgeHandler = this.bridgeHandler;
+ BridgeHandler bridgeHandler = this.bridgeHandler;
if (bridgeHandler == null) {
return null;
}
if (deviceId == null) {
return;
}
- BoschSHCBridgeHandler bridgeHandler = this.bridgeHandler;
+ BridgeHandler bridgeHandler = this.bridgeHandler;
if (bridgeHandler == null) {
return;
}
*/
public void onStateUpdate(JsonElement stateData) {
@Nullable
- TState state = gson.fromJson(stateData, this.stateClass);
+ TState state = BoschSHCServiceState.fromJson(stateData, this.stateClass);
if (state == null) {
this.logger.warn("Received invalid, expected type {}", this.stateClass.getName());
return;
stateUpdateListener.accept(state);
}
}
+
+ /**
+ * Allows a service to handle a command and create a new state out of it.
+ * The new state still has to be set via setState.
+ *
+ * @param command Command to handle
+ * @throws BoschSHCException If service can not handle command
+ */
+ public TState handleCommand(Command command) throws BoschSHCException {
+ throw new BoschSHCException(
+ String.format("%s: Can not handle command %s", this.getServiceName(), command.getClass().getName()));
+ }
}
--- /dev/null
+/**
+ * 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.airqualitylevel;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.services.BoschSHCService;
+import org.openhab.binding.boschshc.internal.services.airqualitylevel.dto.AirQualityLevelServiceState;
+
+/**
+ * This service constantly measures key air quality values to help you create a healthy room climate.
+ *
+ * @author Christian Oeing - Initial contribution
+ */
+@NonNullByDefault
+public class AirQualityLevelService extends BoschSHCService<AirQualityLevelServiceState> {
+
+ public AirQualityLevelService() {
+ super("AirQualityLevel", AirQualityLevelServiceState.class);
+ }
+}
--- /dev/null
+/**
+ * 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.airqualitylevel.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 AirQualityLevelServiceState extends BoschSHCServiceState {
+
+ public AirQualityLevelServiceState() {
+ 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;
+}
--- /dev/null
+/**
+ * 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.childlock;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.services.BoschSHCService;
+import org.openhab.binding.boschshc.internal.services.childlock.dto.ChildLockServiceState;
+import org.openhab.binding.boschshc.internal.services.childlock.dto.ChildLockState;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.types.Command;
+
+/**
+ * Indicates if child lock of device is active.
+ *
+ * @author Christian Oeing - Initial contribution
+ */
+@NonNullByDefault
+public class ChildLockService extends BoschSHCService<ChildLockServiceState> {
+ public ChildLockService() {
+ super("Thermostat", ChildLockServiceState.class);
+ }
+
+ @Override
+ public ChildLockServiceState handleCommand(Command command) throws BoschSHCException {
+ if (command instanceof OnOffType) {
+ ChildLockServiceState state = new ChildLockServiceState();
+ state.childLock = ChildLockState.valueOf(command.toFullString());
+ return state;
+ } else {
+ return super.handleCommand(command);
+ }
+ }
+}
--- /dev/null
+/**
+ * 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.childlock.dto;
+
+import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
+import org.openhab.core.library.types.OnOffType;
+
+/**
+ * State for {@link ChildLockService} to activate and deactivate the child lock
+ * of a device.
+ *
+ * @author Christian Oeing - Initial contribution
+ */
+public class ChildLockServiceState extends BoschSHCServiceState {
+ public ChildLockServiceState() {
+ super("childLockState");
+ }
+
+ public ChildLockState childLock;
+
+ public OnOffType getActiveState() {
+ return OnOffType.from(this.childLock.toString());
+ }
+}
--- /dev/null
+/**
+ * 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.childlock.dto;
+
+/**
+ * Possible values for {@link ChildLockServiceState}.
+ *
+ * @author Christian Oeing - Initial contribution
+ */
+public enum ChildLockState {
+ ON,
+ OFF
+}
*/
package org.openhab.binding.boschshc.internal.services.dto;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
import com.google.gson.annotations.SerializedName;
/**
* @author Christian Oeing - Initial contribution
*/
public class BoschSHCServiceState {
+
+ /**
+ * gson instance to convert a class to json string and back.
+ */
+ private static final Gson gson = new Gson();
+
+ private static final Logger logger = LoggerFactory.getLogger(BoschSHCServiceState.class);
+
+ /**
+ * State type. Initialized when instance is created.
+ */
+ private @Nullable String stateType = null;
+
@SerializedName("@type")
private final String type;
protected BoschSHCServiceState(String type) {
this.type = type;
+
+ if (stateType == null) {
+ stateType = type;
+ }
}
public String getType() {
return type;
}
+
+ protected boolean isValid() {
+ String expectedType = stateType;
+ if (expectedType == null || !expectedType.equals(this.type)) {
+ var className = this.getClass().getName();
+ logger.debug("Expected state type {} for state class {}, received {}", expectedType, className, this.type);
+ return false;
+ }
+
+ return true;
+ }
+
+ public static <TState extends BoschSHCServiceState> @Nullable TState fromJson(String json,
+ Class<TState> stateClass) {
+ var state = gson.fromJson(json, stateClass);
+ if (state == null || !state.isValid()) {
+ return null;
+ }
+
+ return state;
+ }
+
+ public static <TState extends BoschSHCServiceState> @Nullable TState fromJson(JsonElement json,
+ Class<TState> stateClass) {
+ var state = gson.fromJson(json, stateClass);
+ if (state == null || !state.isValid()) {
+ return null;
+ }
+
+ return state;
+ }
}
* @author Christian Oeing - Initial contribution
*/
public class JsonRestExceptionResponse extends BoschSHCServiceState {
+
+ /**
+ * The entity could not be found. One of the defined path parameters was invalid.
+ */
+ public static final String ENTITY_NOT_FOUND = "ENTITY_NOT_FOUND";
+
public JsonRestExceptionResponse() {
super("JsonRestExceptionResponseEntity");
this.errorCode = "";
/**
* The HTTP status of the error.
*/
- public int statusCode;
+ public Integer statusCode;
+
+ public static boolean isValid(JsonRestExceptionResponse obj) {
+ return obj != null && obj.errorCode != null && obj.statusCode != null;
+ }
}
--- /dev/null
+/**
+ * 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.humiditylevel;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.services.BoschSHCService;
+import org.openhab.binding.boschshc.internal.services.humiditylevel.dto.HumidityLevelServiceState;
+
+/**
+ * Measures the humidity at a central point in the room.
+ *
+ * @author Christian Oeing - Initial contribution
+ */
+@NonNullByDefault
+public class HumidityLevelService extends BoschSHCService<HumidityLevelServiceState> {
+
+ public HumidityLevelService() {
+ super("HumidityLevel", HumidityLevelServiceState.class);
+ }
+}
--- /dev/null
+/**
+ * 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.humiditylevel.dto;
+
+import javax.measure.quantity.Dimensionless;
+
+import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.types.State;
+
+/**
+ * State for {@link HumidityLevelService} to get and set the desired temperature of a room.
+ *
+ * @author Christian Oeing - Initial contribution
+ */
+public class HumidityLevelServiceState extends BoschSHCServiceState {
+
+ public HumidityLevelServiceState() {
+ super("humidityLevelState");
+ }
+
+ /**
+ * Current measured humidity.
+ */
+ public double humidity;
+
+ public State getHumidityState() {
+ return new QuantityType<Dimensionless>(this.humidity, Units.PERCENT);
+ }
+}
--- /dev/null
+/**
+ * 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.latestmotion;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.services.BoschSHCService;
+import org.openhab.binding.boschshc.internal.services.latestmotion.dto.LatestMotionServiceState;
+
+/**
+ * Detects every movement through an intelligent combination of passive infra-red technology and an additional
+ * temperature sensor.
+ *
+ * @author Christian Oeing - Initial contribution
+ */
+@NonNullByDefault
+public class LatestMotionService extends BoschSHCService<LatestMotionServiceState> {
+
+ public LatestMotionService() {
+ super("LatestMotion", LatestMotionServiceState.class);
+ }
+}
--- /dev/null
+/**
+ * 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.latestmotion.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 LatestMotionServiceState extends BoschSHCServiceState {
+
+ public LatestMotionServiceState() {
+ super("latestMotionState");
+ }
+
+ public String latestMotionDetected;
+}
--- /dev/null
+/**
+ * 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.powermeter;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.services.BoschSHCService;
+import org.openhab.binding.boschshc.internal.services.powermeter.dto.PowerMeterServiceState;
+
+/**
+ * With this service you always have an eye on energy consumption.
+ *
+ * @author Christian Oeing - Initial contribution
+ */
+@NonNullByDefault
+public class PowerMeterService extends BoschSHCService<PowerMeterServiceState> {
+
+ public PowerMeterService() {
+ super("PowerMeter", PowerMeterServiceState.class);
+ }
+}
--- /dev/null
+/**
+ * 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.powermeter.dto;
+
+import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
+
+/**
+ * State for {@link PowerMeterService}
+ *
+ * @author Stefan Kästle - Initial contribution
+ */
+public class PowerMeterServiceState extends BoschSHCServiceState {
+
+ public PowerMeterServiceState() {
+ super("powerMeterState");
+ }
+
+ public double energyConsumption;
+ public double powerConsumption;
+}
offline.long-polling-failed.http-client-null = Long polling failed and could not be restarted because http client is null.
offline.long-polling-failed.trying-to-reconnect = Long polling failed, will try to reconnect.
offline.interrupted = Conneting to Bosch Smart Home Controller was interrupted.
+
+offline.conf-error.empty-device-id = No device ID set.
+
+offline.conf-error.invalid-device-id = Device ID is invalid.
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.
+offline.conf-error.empty-device-id = Keine Geräte ID gesetzt.
+offline.conf-error.invalid-device-id = Geräte ID ist ungültig.
</supported-bridge-type-refs>
<label>In-wall Switch</label>
- <description>Bosch In-wall switch for light control</description>
+ <description>A simple light control.</description>
<channels>
<channel id="power-switch" typeId="system.power"/>
</supported-bridge-type-refs>
<label>TwinGuard</label>
- <description>Bosch TwinGuard environmental sensor</description>
+ <description>The Twinguard smoke detector warns you in case of fire and constantly monitors the air.</description>
<channels>
<channel id="temperature" typeId="temperature"/>
<bridge-type-ref id="shc"/>
</supported-bridge-type-refs>
- <label>Window/Door Contact</label>
- <description>Bosch Contact for windows and doors</description>
+ <label>Door/Window Contact</label>
+ <description>Detects open windows and doors.</description>
<channels>
<channel id="contact" typeId="contact"/>
</supported-bridge-type-refs>
<label>Motion Detector</label>
- <description>Bosch Motion Detector</description>
+ <description>Detects every movement through an intelligent combination of passive infra-red technology and an
+ additional temperature sensor.</description>
<channels>
<channel id="latest-motion" typeId="latest-motion"/>
</supported-bridge-type-refs>
<label>Shutter Control</label>
- <description>Bosch Shutter Control</description>
+ <description>Control of your shutter to take any position you desire.</description>
<channels>
<channel id="level" typeId="level"/>
</supported-bridge-type-refs>
<label>Thermostat</label>
- <description>Bosch Thermostat</description>
+ <description>Radiator thermostat</description>
<channels>
<channel id="temperature" typeId="temperature"/>
<channel id="valve-tappet-position" typeId="valve-tappet-position"/>
+ <channel id="child-lock" typeId="child-lock"/>
</channels>
<config-description-ref uri="thing-type:boschshc:device"/>
</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
+ <description>This is a virtual device which is automatically created for all rooms that have
thermostats in it.</description>
<channels>
</thing-type>
+ <thing-type id="wall-thermostat">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="shc"/>
+ </supported-bridge-type-refs>
+
+ <label>Wall Thermostat</label>
+ <description>Display of the current room temperature as well as the relative humidity in the room.</description>
+
+ <channels>
+ <channel id="temperature" typeId="temperature"/>
+ <channel id="humidity" typeId="humidity"/>
+ </channels>
+
+ <config-description-ref uri="thing-type:boschshc:device"/>
+
+ </thing-type>
+
<channel-type id="temperature">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<state min="5" max="30" step="0.5" pattern="%.1f %unit%" readOnly="true"/>
</channel-type>
+ <channel-type id="child-lock">
+ <item-type>Switch</item-type>
+ <label>Child Lock</label>
+ <description>Indicates if it is possible to set the desired temperature on the device.</description>
+ </channel-type>
+
</thing:thing-descriptions>
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);
+ httpClient.sendRequest(request, SubscribeResult.class, SubscribeResult::isValid, null);
});
}
}
--- /dev/null
+/**
+ * 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 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;
+import com.google.gson.JsonObject;
+
+/**
+ * Test class
+ *
+ * @author Christian Oeing - Initial contribution
+ */
+class TestState extends BoschSHCServiceState {
+ public TestState() {
+ super("testState");
+ }
+}
+
+/**
+ * Test class
+ *
+ * @author Christian Oeing - Initial contribution
+ */
+class TestState2 extends BoschSHCServiceState {
+ public TestState2() {
+ super("testState2");
+ }
+}
+
+/**
+ * Unit tests for BoschSHCServiceStateTest
+ *
+ * @author Christian Oeing - Initial contribution
+ */
+@NonNullByDefault
+public class BoschSHCServiceStateTest {
+ private final Gson gson = new Gson();
+
+ @Test
+ public void fromJson_nullStateForDifferentType() {
+ var state = BoschSHCServiceState.fromJson(gson.fromJson("{\"@type\":\"differentState\"}", JsonObject.class),
+ TestState.class);
+ assertEquals(null, state);
+ }
+
+ @Test
+ public void fromJson_stateObjectForValidJson() {
+ var state = BoschSHCServiceState.fromJson(gson.fromJson("{\"@type\":\"testState\"}", JsonObject.class),
+ TestState.class);
+ assertNotEquals(null, state);
+ }
+
+ /**
+ * This checks for a bug we had where the expected type stayed the same for different state classes
+ */
+ @Test
+ public void fromJson_stateObjectForValidJsonAfterOtherState() {
+ BoschSHCServiceState.fromJson(gson.fromJson("{\"@type\":\"testState\"}", JsonObject.class), TestState.class);
+ var state2 = BoschSHCServiceState.fromJson(gson.fromJson("{\"@type\":\"testState2\"}", JsonObject.class),
+ TestState2.class);
+ assertNotEquals(null, state2);
+ }
+}