Skip to content

Commit

Permalink
#14195 Bridge and Device Discovery
Browse files Browse the repository at this point in the history
Bridge discovery is implemented via mDNS, local IP addresses are checked.
If a GET returns the public SHC information,
then this shcIpAddress is reported as a discovered bridge.

Devices are always discovered after successful pairing, but a manual scan is also possible.

Added unit tests for Bridge and Device Discovery.

Signed-off-by: Gerd Zanker <[email protected]>
  • Loading branch information
GerdZanker committed Feb 22, 2023
1 parent 6daddb7 commit ea1fdb2
Show file tree
Hide file tree
Showing 10 changed files with 958 additions and 92 deletions.
35 changes: 19 additions & 16 deletions bundles/org.openhab.binding.boschshc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,15 @@ The smoke detector warns you in case of fire.

## Limitations

- Discovery of Things
- Discovery of Bridge
No major limitation known.
Check list of [openhab issues with "boshshc"](https://github.com/openhab/openhab-addons/issues?q=is%3Aissue+boschshc+)

## Discovery

Configuration via configuration files or UI (see below).
Bridge discovery is supported via mDNS.
Things discovery is started after successful pairing.

Configuration via configuration files or UI supported too (see below).

## Bridge Configuration

Expand Down Expand Up @@ -239,19 +242,19 @@ Alternatively, the log can be viewed using the OpenHab Log Viewer (frontail) via
Example:

```bash
2020-08-11 12:42:49.490 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Heizung id=hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX
2020-08-11 12:42:49.495 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=-RoomClimateControl- id=roomClimateControl_hz_1
2020-08-11 12:42:49.497 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=-VentilationService- id=ventilationService
2020-08-11 12:42:49.498 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Großes Fenster id=hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX
2020-08-11 12:42:49.501 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=-IntrusionDetectionSystem- id=intrusionDetectionSystem
2020-08-11 12:42:49.502 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Rollladen id=hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX
2020-08-11 12:42:49.502 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Heizung id=hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX
2020-08-11 12:42:49.503 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Heizung Haus id=hdm:ICom:819410185:HC1
2020-08-11 12:42:49.503 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=-RoomClimateControl- id=roomClimateControl_hz_6
2020-08-11 12:42:49.504 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=PhilipsHueBridgeManager id=hdm:PhilipsHueBridge:PhilipsHueBridgeManager
2020-08-11 12:42:49.505 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Rollladen id=hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX
2020-08-11 12:42:49.506 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Rollladen id=hdm:HomeMaticIP:3014F711A000XXXXXXXXXXXX
2020-08-11 12:42:49.507 [INFO ] [chshc.internal.BoschSHCBridgeHandler] - Found device: name=Central Heating id=hdm:ICom:819410185
2023-01-23 20:28:54.725 [INFO ] [rnal.discovery.ThingDiscoveryService] - Found device: name=RoomLightControl id=roomLightControl_hz_XXXXXXXX
2023-01-23 20:28:54.725 [INFO ] [rnal.discovery.ThingDiscoveryService] - .... service: BinarySwitch
2023-01-23 20:28:54.725 [INFO ] [rnal.discovery.ThingDiscoveryService] - Found device: name=Zimmer1 Rauchmelder id=hdm:HomeMaticIP:3014F711XXXXXXXXXXXXXXXX
2023-01-23 20:28:54.725 [INFO ] [rnal.discovery.ThingDiscoveryService] - .... service: BatteryLevel
2023-01-23 20:28:54.725 [INFO ] [rnal.discovery.ThingDiscoveryService] - .... service: Alarm
2023-01-23 20:28:54.725 [INFO ] [rnal.discovery.ThingDiscoveryService] - .... service: SmokeDetectorCheck
2023-01-23 20:28:54.725 [INFO ] [rnal.discovery.ThingDiscoveryService] - Found device: name=-RoomClimateControl- id=roomClimateControl_hz_XXXXXXXX
2023-01-23 20:28:54.725 [INFO ] [rnal.discovery.ThingDiscoveryService] - .... service: TemperatureLevelConfiguration
2023-01-23 20:28:54.726 [INFO ] [rnal.discovery.ThingDiscoveryService] - .... service: ThermostatSupportedControlMode
2023-01-23 20:28:54.726 [INFO ] [rnal.discovery.ThingDiscoveryService] - .... service: RoomClimateControl
2023-01-23 20:28:54.726 [INFO ] [rnal.discovery.ThingDiscoveryService] - .... service: TemperatureLevel
2023-01-23 20:28:54.738 [INFO ] [rnal.discovery.ThingDiscoveryService] - Found device: name=Zimmer 3 Rollo id=hdm:HomeMaticIP:3014F711XXXXXXXXXXXXXXXX
2023-01-23 20:28:54.738 [INFO ] [rnal.discovery.ThingDiscoveryService] - .... service: ShutterControl
```

## Thing Configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,11 +316,12 @@ public <TContent> TContent sendRequest(Request request, Class<TContent> response
if (errorResponseHandler != null) {
throw errorResponseHandler.apply(statusCode, textContent);
} else {
throw new ExecutionException(String.format("Request failed with status code %s", statusCode), null);
throw new ExecutionException(String.format("Send request failed with status code %s", statusCode),
null);
}
}

logger.debug("Received response: {} - status: {}", textContent, statusCode);
logger.debug("Send request completed with success: {} - status code: {}", textContent, statusCode);

try {
@Nullable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import static org.eclipse.jetty.http.HttpMethod.*;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
Expand All @@ -33,6 +33,7 @@
import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
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.discovery.ThingDiscoveryService;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException;
Expand All @@ -45,6 +46,7 @@
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
Expand All @@ -62,6 +64,7 @@
* @author Gerd Zanker - added HttpClient with pairing support
* @author Christian Oeing - refactorings of e.g. server registration
* @author David Pace - Added support for custom endpoints and HTTP POST requests
* @author Gerd Zanker - added thing discovery
*/
@NonNullByDefault
public class BridgeHandler extends BaseBridgeHandler {
Expand All @@ -88,12 +91,24 @@ public class BridgeHandler extends BaseBridgeHandler {

private @Nullable ScheduledFuture<?> scheduledPairing;

/**
* SHC thing/device discovery service instance.
* Registered and unregistered if service is actived/deactived.
* Used to scan for things after bridge is paired with SHC.
*/
private @Nullable ThingDiscoveryService thingDiscoveryService;

public BridgeHandler(Bridge bridge) {
super(bridge);

this.longPolling = new LongPolling(this.scheduler, this::handleLongPollResult, this::handleLongPollFailure);
}

@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(ThingDiscoveryService.class);
}

@Override
public void initialize() {
Bundle bundle = FrameworkUtil.getBundle(getClass());
Expand Down Expand Up @@ -225,19 +240,21 @@ private void scheduleInitialAccess(BoschHttpClient httpClient) {
return;
}

// SHC is online and access is possible
// print rooms and devices
boolean thingReachable = true;
thingReachable &= this.getRooms();
thingReachable &= this.getDevices();
if (!thingReachable) {
// SHC is online and access should possible
if (!checkBridgeAccess()) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"@text/offline.not-reachable");
// restart initial access
scheduleInitialAccess(httpClient);
return;
}

// do thing discovery after pairing
final ThingDiscoveryService discovery = thingDiscoveryService;
if (discovery != null) {
discovery.doScan();
}

// start long polling loop
this.updateStatus(ThingStatus.ONLINE);
try {
Expand All @@ -252,54 +269,132 @@ private void scheduleInitialAccess(BoschHttpClient httpClient) {
}
}

/**
* Check the bridge access by sending an HTTP request.
* Does not throw any exception in case the request fails.
*/
public boolean checkBridgeAccess() {
@Nullable
BoschHttpClient httpClient = this.httpClient;

if (httpClient == null) {
return false;
}

try {
logger.debug("Sending http request to BoschSHC to check access: {}", 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("Access check failed with status code: {}", contentResponse.getStatus());
return false;
}

// Access OK
return true;
} catch (TimeoutException | ExecutionException | InterruptedException e) {
logger.warn("Access check failed because of {}!", e.getMessage());
return false;
}
}

/**
* Get a list of connected devices from the Smart-Home Controller
*
* @throws InterruptedException in case bridge is stopped
*/
private boolean getDevices() throws InterruptedException {
public List<Device> getDevices() throws InterruptedException {
List<Device> emptyDevices = new ArrayList<Device>();

@Nullable
BoschHttpClient httpClient = this.httpClient;
if (httpClient == null) {
return false;
return emptyDevices;
}

try {
logger.debug("Sending http request to Bosch to request devices: {}", httpClient);
logger.trace("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;
return emptyDevices;
}

String content = contentResponse.getContentAsString();
logger.debug("Request devices completed with success: {} - status code: {}", content,
logger.trace("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);
}
}
@Nullable
List<Device> nullableDevices = gson.fromJson(content, collectionType);
return Objects.requireNonNullElse(nullableDevices, emptyDevices);
} catch (TimeoutException | ExecutionException e) {
logger.debug("Request devices failed because of {}!", e.getMessage());
return emptyDevices;
}
}

/**
* Get a list of rooms from the Smart-Home controller
*
* @throws InterruptedException in case bridge is stopped
*/
public List<Room> getRooms() throws InterruptedException {
List<Room> emptyRooms = new ArrayList<>();
@Nullable
BoschHttpClient httpClient = this.httpClient;
if (httpClient != null) {
try {
logger.trace("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 emptyRooms;
}

String content = contentResponse.getContentAsString();
logger.trace("Request rooms completed with success: {} - status code: {}", content,
contentResponse.getStatus());

Type collectionType = new TypeToken<ArrayList<Room>>() {
}.getType();

ArrayList<Room> rooms = gson.fromJson(content, collectionType);
return Objects.requireNonNullElse(rooms, emptyRooms);
} catch (TimeoutException | ExecutionException e) {
logger.debug("Request rooms failed because of {}!", e.getMessage());
return emptyRooms;
}
} catch (TimeoutException | ExecutionException e) {
logger.warn("Request devices failed because of {}!", e.getMessage());
return false;
} else {
return emptyRooms;
}
}

public boolean registerDiscoveryListener(ThingDiscoveryService listener) {
if (thingDiscoveryService == null) {
thingDiscoveryService = listener;
return true;
}

return true;
return false;
}

public boolean unregisterDiscoveryListener() {
if (thingDiscoveryService != null) {
thingDiscoveryService = null;
return true;
}

return false;
}

/**
Expand Down Expand Up @@ -420,51 +515,6 @@ private void handleLongPollFailure(Throwable e) {
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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2023 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.bridge.dto;

import java.util.List;

/**
* Public Information of the controller.
*
* Currently, only the ipAddress is used for discovery. More fields can be added on demand.
*
* Json example:
* {
* "apiVersions":["1.2","2.1"],
* ...
* "shcIpAddress":"192.168.1.2",
* ...
* }
*
* @author Gerd Zanker - Initial contribution
*/
public class PublicInformation {
public PublicInformation() {
this.shcIpAddress = "";
this.shcGeneration = "";
}

public List<String> apiVersions;
public String shcIpAddress;
public String shcGeneration;
}
Loading

0 comments on commit ea1fdb2

Please sign in to comment.