[boschshc] Bridge and Device Discovery (#14197)

* #14195 Bridge and Device Discovery

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 <gerd.zanker@web.de>
This commit is contained in:
Gerd Zanker
2023-03-25 00:26:24 +01:00
committed by GitHub
parent c30893281c
commit 3b08217ff7
16 changed files with 1045 additions and 109 deletions

View File

@@ -53,7 +53,6 @@ public abstract class BoschSHCDeviceHandler extends BoschSHCHandler {
@Override
public void initialize() {
var config = this.config = getConfigAs(BoschSHCConfiguration.class);
String deviceId = config.id;

View File

@@ -124,7 +124,6 @@ public abstract class BoschSHCHandler extends BaseThingHandler {
*/
@Override
public void initialize() {
// Initialize device services
try {
this.initializeServices();
@@ -304,7 +303,6 @@ public abstract class BoschSHCHandler extends BaseThingHandler {
protected <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void registerService(
TService service, Consumer<TState> stateUpdateListener, Collection<String> affectedChannels,
boolean shouldFetchInitialState) throws BoschSHCException {
String deviceId = verifyBoschID();
service.initialize(getBridgeHandler(), deviceId, stateUpdateListener);
this.registerService(service, affectedChannels);
@@ -325,7 +323,6 @@ public abstract class BoschSHCHandler extends BaseThingHandler {
*/
private <TService extends BoschSHCService<TState>, TState extends BoschSHCServiceState> void fetchInitialState(
TService service, Consumer<TState> stateUpdateListener) {
try {
@Nullable
TState serviceState = service.getState();
@@ -353,7 +350,6 @@ public abstract class BoschSHCHandler extends BaseThingHandler {
*/
protected <TService extends AbstractBoschSHCService> void registerStatelessService(TService service)
throws BoschSHCException {
String deviceId = verifyBoschID();
service.initialize(getBridgeHandler(), deviceId);
// do not register in service list because the service can not receive state updates

View File

@@ -65,13 +65,22 @@ public class BoschHttpClient extends HttpClient {
}
/**
* Returns the public information URL for the Bosch SHC clients, using port 8446.
* Returns the public information URL for the Bosch SHC client addressed with the given IP address, using port 8446
* See https://github.com/BoschSmartHome/bosch-shc-api-docs/blob/master/postman/README.md
*
* @return URL for public information
*/
public static String getPublicInformationUrl(String ipAddress) {
return String.format("https://%s:8446/smarthome/public/information", ipAddress);
}
/**
* Returns the public information URL for the current Bosch SHC client.
*
* @return URL for public information
*/
public String getPublicInformationUrl() {
return String.format("https://%s:8446/smarthome/public/information", this.ipAddress);
return getPublicInformationUrl(this.ipAddress);
}
/**
@@ -316,11 +325,12 @@ public class BoschHttpClient extends HttpClient {
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

View File

@@ -16,6 +16,11 @@ import static org.eclipse.jetty.http.HttpMethod.*;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@@ -33,6 +38,7 @@ import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
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;
@@ -45,6 +51,7 @@ 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.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
@@ -62,6 +69,7 @@ import com.google.gson.reflect.TypeToken;
* @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 {
@@ -88,12 +96,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());
@@ -225,12 +245,8 @@ public class BridgeHandler extends BaseBridgeHandler {
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
@@ -238,6 +254,12 @@ public class BridgeHandler extends BaseBridgeHandler {
return;
}
// do thing discovery after pairing
final ThingDiscoveryService discovery = thingDiscoveryService;
if (discovery != null) {
discovery.doScan();
}
// start long polling loop
this.updateStatus(ThingStatus.ONLINE);
try {
@@ -253,53 +275,129 @@ public class BridgeHandler extends BaseBridgeHandler {
}
/**
* Get a list of connected devices from the Smart-Home Controller
*
* @throws InterruptedException in case bridge is stopped
* Check the bridge access by sending an HTTP request.
* Does not throw any exception in case the request fails.
*/
private boolean getDevices() throws InterruptedException {
public boolean checkBridgeAccess() throws InterruptedException {
@Nullable
BoschHttpClient httpClient = this.httpClient;
if (httpClient == null) {
return false;
}
try {
logger.debug("Sending http request to Bosch to request devices: {}", httpClient);
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 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
*/
public List<Device> getDevices() throws InterruptedException {
@Nullable
BoschHttpClient httpClient = this.httpClient;
if (httpClient == null) {
return Collections.emptyList();
}
try {
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 Collections.emptyList();
}
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 Optional.ofNullable(nullableDevices).orElse(Collections.emptyList());
} catch (TimeoutException | ExecutionException e) {
logger.warn("Request devices failed because of {}!", e.getMessage());
return false;
logger.debug("Request devices failed because of {}!", e.getMessage());
return Collections.emptyList();
}
}
/**
* 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;
}
} 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;
}
/**
@@ -420,51 +518,6 @@ public class BridgeHandler extends BaseBridgeHandler {
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

View File

@@ -0,0 +1,45 @@
/**
* 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.
* <p>
*
* Currently, only the ipAddress is used for discovery. More fields can be added on demand.
* <p>
* Json example:
*
* <pre>
* {
* "apiVersions":["1.2","2.1"],
* ...
* "shcIpAddress":"192.168.1.2",
* ...
* }
* </pre>
*
* @author Gerd Zanker - Initial contribution
*/
public class PublicInformation {
public PublicInformation() {
this.shcIpAddress = "";
this.shcGeneration = "";
}
public List<String> apiVersions;
public String shcIpAddress;
public String shcGeneration;
}

View File

@@ -0,0 +1,161 @@
/**
* 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.discovery;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.BINDING_ID;
import java.time.Duration;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.binding.boschshc.internal.devices.bridge.BoschHttpClient;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.PublicInformation;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* The {@link BridgeDiscoveryParticipant} is responsible discovering the
* Bosch Smart Home Controller as a Bridge with the mDNS services.
*
* @author Gerd Zanker - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "discovery.boschsmarthomebridge")
public class BridgeDiscoveryParticipant implements MDNSDiscoveryParticipant {
private static final long TTL_SECONDS = Duration.ofHours(1).toSeconds();
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(BoschSHCBindingConstants.THING_TYPE_SHC);
private final Logger logger = LoggerFactory.getLogger(BridgeDiscoveryParticipant.class);
private final HttpClient httpClient;
private final Gson gson = new Gson();
/// SHC Bridge Information, read via public REST API if bridge is detected. Otherwise, strings are empty.
private PublicInformation bridgeInformation = new PublicInformation();
@Activate
public BridgeDiscoveryParticipant(@Reference HttpClientFactory httpClientFactory) {
// create http client upfront to later request public information from SHC
SslContextFactory sslContextFactory = new SslContextFactory.Client.Client(true); // Accept all certificates
sslContextFactory.setTrustAll(true);
sslContextFactory.setValidateCerts(false);
sslContextFactory.setValidatePeerCerts(false);
sslContextFactory.setEndpointIdentificationAlgorithm(null);
httpClient = httpClientFactory.createHttpClient(BINDING_ID, sslContextFactory);
}
protected BridgeDiscoveryParticipant(HttpClient customHttpClient) {
httpClient = customHttpClient;
}
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return SUPPORTED_THING_TYPES_UIDS;
}
@Override
public String getServiceType() {
return "_http._tcp.local.";
}
@Override
public @Nullable DiscoveryResult createResult(ServiceInfo serviceInfo) {
logger.trace("Bridge Discovery started for {}", serviceInfo);
@Nullable
final ThingUID uid = getThingUID(serviceInfo);
if (uid == null) {
return null;
}
logger.trace("Discovered Bosch Smart Home Controller at {}", bridgeInformation.shcIpAddress);
return DiscoveryResultBuilder.create(uid)
.withLabel("Bosch Smart Home Controller (" + bridgeInformation.shcIpAddress + ")")
.withProperty("ipAddress", bridgeInformation.shcIpAddress)
.withProperty("shcGeneration", bridgeInformation.shcGeneration)
.withProperty("apiVersions", bridgeInformation.apiVersions).withTTL(TTL_SECONDS).build();
}
@Override
public @Nullable ThingUID getThingUID(ServiceInfo serviceInfo) {
String ipAddress = discoverBridge(serviceInfo).shcIpAddress;
if (!ipAddress.isBlank()) {
return new ThingUID(BoschSHCBindingConstants.THING_TYPE_SHC, ipAddress.replace('.', '-'));
}
return null;
}
protected PublicInformation discoverBridge(ServiceInfo serviceInfo) {
logger.trace("Discovering serviceInfo {}", serviceInfo);
if (serviceInfo.getHostAddresses() != null && serviceInfo.getHostAddresses().length > 0
&& !serviceInfo.getHostAddresses()[0].isEmpty()) {
String address = serviceInfo.getHostAddresses()[0];
logger.trace("Discovering InetAddress {}", address);
// store all information for later access
bridgeInformation = getPublicInformationFromPossibleBridgeAddress(address);
}
return bridgeInformation;
}
protected PublicInformation getPublicInformationFromPossibleBridgeAddress(String ipAddress) {
String url = BoschHttpClient.getPublicInformationUrl(ipAddress);
logger.trace("Discovering ipAddress {}", url);
try {
httpClient.start();
ContentResponse contentResponse = httpClient.newRequest(url).method(HttpMethod.GET).send();
// check HTTP status code
if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
logger.debug("Discovering failed with status code: {}", contentResponse.getStatus());
return new PublicInformation();
}
// get content from response
String content = contentResponse.getContentAsString();
logger.trace("Discovered SHC - public info {}", content);
PublicInformation bridgeInfo = gson.fromJson(content, PublicInformation.class);
if (bridgeInfo.shcIpAddress != null) {
return bridgeInfo;
}
} catch (TimeoutException | ExecutionException e) {
logger.debug("Discovering failed with exception {}", e.getMessage());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Exception e) {
logger.debug("Discovering failed during http client request {}", e.getMessage());
}
return new PublicInformation();
}
}

View File

@@ -0,0 +1,253 @@
/**
* 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.discovery;
import java.util.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.binding.boschshc.internal.devices.bridge.BridgeHandler;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ThingDiscoveryService} is responsible to discover Bosch Smart Home things.
* The paired SHC BridgeHandler is required to get the lists of rooms and devices.
* With this data the openhab things are discovered.
*
* The order to make this work is
* 1. SHC bridge is created, e.v via openhab UI
* 2. Service is instantiated setBridgeHandler of this service is called
* 3. Service is activated
* 4. Service registers itself as discoveryLister at the bridge
* 5. bridge calls startScan after bridge is paired and things can be discovered
*
* @author Gerd Zanker - Initial contribution
*/
@NonNullByDefault
public class ThingDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
private static final int SEARCH_TIME = 1;
private final Logger logger = LoggerFactory.getLogger(ThingDiscoveryService.class);
private @Nullable BridgeHandler shcBridgeHandler;
protected static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(
BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH, BoschSHCBindingConstants.THING_TYPE_TWINGUARD,
BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT, BoschSHCBindingConstants.THING_TYPE_MOTION_DETECTOR,
BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL, BoschSHCBindingConstants.THING_TYPE_THERMOSTAT,
BoschSHCBindingConstants.THING_TYPE_CLIMATE_CONTROL, BoschSHCBindingConstants.THING_TYPE_WALL_THERMOSTAT,
BoschSHCBindingConstants.THING_TYPE_CAMERA_360, BoschSHCBindingConstants.THING_TYPE_CAMERA_EYES,
BoschSHCBindingConstants.THING_TYPE_INTRUSION_DETECTION_SYSTEM,
BoschSHCBindingConstants.THING_TYPE_SMART_PLUG_COMPACT, BoschSHCBindingConstants.THING_TYPE_SMART_BULB,
BoschSHCBindingConstants.THING_TYPE_SMOKE_DETECTOR);
// @formatter:off
protected static final Map<String, ThingTypeUID> DEVICEMODEL_TO_THINGTYPE_MAP = Map.ofEntries(
new AbstractMap.SimpleEntry<>("BBL", BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL),
new AbstractMap.SimpleEntry<>("TWINGUARD", BoschSHCBindingConstants.THING_TYPE_TWINGUARD),
new AbstractMap.SimpleEntry<>("PSM", BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH),
new AbstractMap.SimpleEntry<>("PLUG_COMPACT", BoschSHCBindingConstants.THING_TYPE_SMART_PLUG_COMPACT),
new AbstractMap.SimpleEntry<>("CAMERA_360", BoschSHCBindingConstants.THING_TYPE_CAMERA_360),
new AbstractMap.SimpleEntry<>("CAMERA_EYES", BoschSHCBindingConstants.THING_TYPE_CAMERA_EYES),
new AbstractMap.SimpleEntry<>("BWTH", BoschSHCBindingConstants.THING_TYPE_THERMOSTAT), // wall thermostat
new AbstractMap.SimpleEntry<>("THB", BoschSHCBindingConstants.THING_TYPE_WALL_THERMOSTAT), // wall thermostat with batteries
new AbstractMap.SimpleEntry<>("SD", BoschSHCBindingConstants.THING_TYPE_SMOKE_DETECTOR),
new AbstractMap.SimpleEntry<>("MD", BoschSHCBindingConstants.THING_TYPE_MOTION_DETECTOR),
new AbstractMap.SimpleEntry<>("ROOM_CLIMATE_CONTROL", BoschSHCBindingConstants.THING_TYPE_CLIMATE_CONTROL),
new AbstractMap.SimpleEntry<>("INTRUSION_DETECTION_SYSTEM", BoschSHCBindingConstants.THING_TYPE_INTRUSION_DETECTION_SYSTEM),
new AbstractMap.SimpleEntry<>("HUE_LIGHT", BoschSHCBindingConstants.THING_TYPE_SMART_BULB),
new AbstractMap.SimpleEntry<>("WRC2", BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT)
// Future Extension: map deviceModel names to BoschSHC Thing Types when they are supported
// new AbstractMap.SimpleEntry<>("SMOKE_DETECTION_SYSTEM", BoschSHCBindingConstants.),
// new AbstractMap.SimpleEntry<>("PRESENCE_SIMULATION_SERVICE", BoschSHCBindingConstants.),
// new AbstractMap.SimpleEntry<>("VENTILATION_SERVICE", BoschSHCBindingConstants.),
// new AbstractMap.SimpleEntry<>("HUE_BRIDGE", BoschSHCBindingConstants.)
// new AbstractMap.SimpleEntry<>("HUE_BRIDGE_MANAGER*", BoschSHCBindingConstants.)
// new AbstractMap.SimpleEntry<>("HUE_LIGHT_ROOM_CONTROL", BoschSHCBindingConstants.)
);
// @formatter:on
public ThingDiscoveryService() {
super(SUPPORTED_THING_TYPES, SEARCH_TIME);
}
@Override
public void activate() {
logger.trace("activate");
final BridgeHandler handler = shcBridgeHandler;
if (handler != null) {
handler.registerDiscoveryListener(this);
}
}
@Override
public void deactivate() {
logger.trace("deactivate");
final BridgeHandler handler = shcBridgeHandler;
if (handler != null) {
removeOlderResults(new Date().getTime(), handler.getThing().getUID());
handler.unregisterDiscoveryListener();
}
super.deactivate();
}
@Override
protected void startScan() {
if (shcBridgeHandler == null) {
logger.debug("The shcBridgeHandler is empty, no manual scan is currently possible");
return;
}
try {
doScan();
} catch (InterruptedException e) {
// Restore interrupted state...
Thread.currentThread().interrupt();
}
}
@Override
protected synchronized void stopScan() {
logger.debug("Stop manual scan on bridge {}",
shcBridgeHandler != null ? shcBridgeHandler.getThing().getUID() : "?");
super.stopScan();
final BridgeHandler handler = shcBridgeHandler;
if (handler != null) {
removeOlderResults(getTimestampOfLastScan(), handler.getThing().getUID());
}
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof BridgeHandler) {
logger.trace("Set bridge handler {}", handler);
shcBridgeHandler = (BridgeHandler) handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return shcBridgeHandler;
}
public void doScan() throws InterruptedException {
logger.debug("Start manual scan on bridge {}", shcBridgeHandler.getThing().getUID());
// use shcBridgeHandler to getDevices()
List<Room> rooms = shcBridgeHandler.getRooms();
logger.debug("SHC has {} rooms", rooms.size());
List<Device> devices = shcBridgeHandler.getDevices();
logger.debug("SHC has {} devices", devices.size());
// Write found devices into openhab.log to support manual configuration
for (Device d : devices) {
logger.debug("Found device: name={} id={}", d.name, d.id);
if (d.deviceServiceIds != null) {
for (String s : d.deviceServiceIds) {
logger.debug(".... service: {}", s);
}
}
}
addDevices(devices, rooms);
}
protected void addDevices(List<Device> devices, List<Room> rooms) {
for (Device device : devices) {
addDevice(device, getRoomNameForDevice(device, rooms));
}
}
protected String getRoomNameForDevice(Device device, List<Room> rooms) {
return rooms.stream().filter(room -> room.id.equals(device.roomId)).findAny().map(r -> r.name).orElse("");
}
protected void addDevice(Device device, String roomName) {
// see startScan for the runtime null check of shcBridgeHandler
assert shcBridgeHandler != null;
logger.trace("Discovering device {}", device.name);
logger.trace("- details: id {}, roomId {}, deviceModel {}", device.id, device.roomId, device.deviceModel);
ThingTypeUID thingTypeUID = getThingTypeUID(device);
if (thingTypeUID == null) {
return;
}
logger.trace("- got thingTypeID '{}' for deviceModel '{}'", thingTypeUID.getId(), device.deviceModel);
ThingUID thingUID = new ThingUID(thingTypeUID, shcBridgeHandler.getThing().getUID(),
device.id.replace(':', '_'));
logger.trace("- got thingUID '{}' for device: '{}'", thingUID, device);
DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID)
.withProperty("id", device.id).withLabel(getNiceName(device.name, roomName));
if (null != shcBridgeHandler) {
discoveryResult.withBridge(shcBridgeHandler.getThing().getUID());
}
if (!roomName.isEmpty()) {
discoveryResult.withProperty("Location", roomName);
}
thingDiscovered(discoveryResult.build());
logger.debug("Discovered device '{}' with thingTypeUID={}, thingUID={}, id={}, deviceModel={}", device.name,
thingUID, thingTypeUID, device.id, device.deviceModel);
}
private String getNiceName(String name, String roomName) {
if (!name.startsWith("-"))
return name;
// convert "-IntrusionDetectionSystem-" into "Intrusion Detection System"
// convert "-RoomClimateControl-" into "Room Climate Control myRoomName"
final char[] chars = name.toCharArray();
StringBuilder niceNameBuilder = new StringBuilder(32);
for (int pos = 0; pos < chars.length; pos++) {
// skip "-"
if (chars[pos] == '-') {
continue;
}
// convert "CamelCase" into "Camel Case", skipping the first Uppercase after the "-"
if (pos > 1 && Character.getType(chars[pos]) == Character.UPPERCASE_LETTER) {
niceNameBuilder.append(" ");
}
niceNameBuilder.append(chars[pos]);
}
// append roomName for "Room Climate Control", because it appears for each room with a thermostat
if (!roomName.isEmpty() && niceNameBuilder.toString().startsWith("Room Climate Control")) {
niceNameBuilder.append(" ").append(roomName);
}
return niceNameBuilder.toString();
}
protected @Nullable ThingTypeUID getThingTypeUID(Device device) {
@Nullable
ThingTypeUID thingTypeId = DEVICEMODEL_TO_THINGTYPE_MAP.get(device.deviceModel);
if (thingTypeId != null) {
return new ThingTypeUID(BoschSHCBindingConstants.BINDING_ID, thingTypeId.getId());
}
logger.debug("Unknown deviceModel '{}'! Please create a support request issue for this unknown device model.",
device.deviceModel);
return null;
}
}

View File

@@ -70,7 +70,6 @@ public abstract class BoschSHCSystemService<TState extends BoschSHCServiceState>
@Override
public @Nullable TState getState()
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
BridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler == null) {
return null;

View File

@@ -38,12 +38,10 @@ public class BatteryLevelService extends BoschSHCService<DeviceServiceData> {
@Override
public @Nullable DeviceServiceData getState()
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
String deviceId = getDeviceId();
if (deviceId == null) {
return null;
}
BridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler == null) {
return null;

View File

@@ -30,9 +30,12 @@ public class BoschSHCServiceState {
/**
* gson instance to convert a class to json string and back.
*/
private static final Gson gson = new Gson();
private static final Gson GSON = new Gson();
private static final Logger logger = LoggerFactory.getLogger(BoschSHCServiceState.class);
/**
* Logger marked as transient to exclude the logger from JSON serialization.
*/
private final transient Logger logger = LoggerFactory.getLogger(BoschSHCServiceState.class);
/**
* State type. Initialized when instance is created.
@@ -67,7 +70,7 @@ public class BoschSHCServiceState {
public static <TState extends BoschSHCServiceState> @Nullable TState fromJson(String json,
Class<TState> stateClass) {
var state = gson.fromJson(json, stateClass);
var state = GSON.fromJson(json, stateClass);
if (state == null || !state.isValid()) {
return null;
}
@@ -77,7 +80,7 @@ public class BoschSHCServiceState {
public static <TState extends BoschSHCServiceState> @Nullable TState fromJson(JsonElement json,
Class<TState> stateClass) {
var state = gson.fromJson(json, stateClass);
var state = GSON.fromJson(json, stateClass);
if (state == null || !state.isValid()) {
return null;
}

View File

@@ -27,7 +27,6 @@ public enum SmokeDetectorCheckState {
SMOKE_TEST_FAILED;
public static SmokeDetectorCheckState from(String stateString) {
try {
return SmokeDetectorCheckState.valueOf(stateString);
} catch (Exception a) {

View File

@@ -135,6 +135,6 @@ offline.not-reachable = The Bosch Smart Home Controller is not reachable.
offline.conf-error-ssl = The SSL connection to the Bosch Smart Home Controller is not possible.
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.interrupted = Connection 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.

View File

@@ -195,7 +195,7 @@ class BoschHttpClientTest {
when(response.getStatus()).thenReturn(500);
ExecutionException e = assertThrows(ExecutionException.class,
() -> httpClient.sendRequest(request, SubscribeResult.class, SubscribeResult::isValid, null));
assertEquals("Request failed with status code 500", e.getMessage());
assertEquals("Send request failed with status code 500", e.getMessage());
}
@Test

View File

@@ -0,0 +1,190 @@
/**
* 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.discovery;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import javax.jmdns.ServiceInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.thing.ThingUID;
/**
* BridgeDiscoveryParticipant Tester.
*
* @author Gerd Zanker - Initial contribution
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault
public class BridgeDiscoveryParticipantTest {
@Nullable
private BridgeDiscoveryParticipant fixture;
private final String url = "https://192.168.0.123:8446/smarthome/public/information";
private @Mock @NonNullByDefault({}) ServiceInfo shcBridge;
private @Mock @NonNullByDefault({}) ServiceInfo otherDevice;
@BeforeEach
public void beforeEach() throws Exception {
when(shcBridge.getHostAddresses()).thenReturn(new String[] { "192.168.0.123" });
when(otherDevice.getHostAddresses()).thenReturn(new String[] { "192.168.0.1" });
ContentResponse contentResponse = mock(ContentResponse.class);
when(contentResponse.getContentAsString()).thenReturn(
"{\"apiVersions\":[\"2.9\",\"3.2\"], \"shcIpAddress\":\"192.168.0.123\", \"shcGeneration\":\"SHC_1\"}");
when(contentResponse.getStatus()).thenReturn(HttpStatus.OK_200);
Request mockRequest = mock(Request.class);
when(mockRequest.send()).thenReturn(contentResponse);
when(mockRequest.method((HttpMethod) any())).thenReturn(mockRequest);
HttpClient mockHttpClient = spy(HttpClient.class); // spy needed, because some final methods can't be mocked
when(mockHttpClient.newRequest(url)).thenReturn(mockRequest);
fixture = new BridgeDiscoveryParticipant(mockHttpClient);
}
/**
*
* Method: getSupportedThingTypeUIDs()
*
*/
@Test
public void testGetSupportedThingTypeUIDs() {
assert fixture != null;
assertTrue(fixture.getSupportedThingTypeUIDs().contains(BoschSHCBindingConstants.THING_TYPE_SHC));
}
/**
*
* Method: getServiceType()
*
*/
@Test
public void testGetServiceType() throws Exception {
assert fixture != null;
assertThat(fixture.getServiceType(), is("_http._tcp.local."));
}
@Test
public void testCreateResult() throws Exception {
assert fixture != null;
DiscoveryResult result = fixture.createResult(shcBridge);
assertNotNull(result);
assertThat(result.getBindingId(), is(BoschSHCBindingConstants.BINDING_ID));
assertThat(result.getThingUID().getId(), is("192-168-0-123"));
assertThat(result.getThingTypeUID().getId(), is("shc"));
assertThat(result.getLabel(), is("Bosch Smart Home Controller (192.168.0.123)"));
}
@Test
public void testCreateResultOtherDevice() throws Exception {
assert fixture != null;
DiscoveryResult result = fixture.createResult(otherDevice);
assertNull(result);
}
@Test
public void testGetThingUID() throws Exception {
assert fixture != null;
ThingUID thingUID = fixture.getThingUID(shcBridge);
assertNotNull(thingUID);
assertThat(thingUID.getBindingId(), is(BoschSHCBindingConstants.BINDING_ID));
assertThat(thingUID.getId(), is("192-168-0-123"));
}
@Test
public void testGetThingUIDOtherDevice() throws Exception {
assert fixture != null;
assertNull(fixture.getThingUID(otherDevice));
}
@Test
public void testGetBridgeAddress() throws Exception {
assert fixture != null;
assertThat(fixture.discoverBridge(shcBridge).shcIpAddress, is("192.168.0.123"));
}
@Test
public void testGetBridgeAddressOtherDevice() throws Exception {
assert fixture != null;
assertThat(fixture.discoverBridge(otherDevice).shcIpAddress, is(""));
}
@Test
public void testGetPublicInformationFromPossibleBridgeAddress() throws Exception {
assert fixture != null;
assertThat(fixture.getPublicInformationFromPossibleBridgeAddress("192.168.0.123").shcIpAddress,
is("192.168.0.123"));
}
@Test
public void testGetPublicInformationFromPossibleBridgeAddressInvalidContent() throws Exception {
assert fixture != null;
ContentResponse contentResponse = mock(ContentResponse.class);
when(contentResponse.getContentAsString()).thenReturn("{\"nothing\":\"useful\"}");
when(contentResponse.getStatus()).thenReturn(HttpStatus.OK_200);
Request mockRequest = mock(Request.class);
when(mockRequest.send()).thenReturn(contentResponse);
when(mockRequest.method((HttpMethod) any())).thenReturn(mockRequest);
HttpClient mockHttpClient = spy(HttpClient.class); // spy needed, because some final methods can't be mocked
when(mockHttpClient.newRequest(url)).thenReturn(mockRequest);
fixture = new BridgeDiscoveryParticipant(mockHttpClient);
assertThat(fixture.getPublicInformationFromPossibleBridgeAddress("shcAddress").shcIpAddress, is(""));
}
@Test
public void testGetPublicInformationFromPossibleBridgeAddressInvalidStatus() throws Exception {
assert fixture != null;
ContentResponse contentResponse = mock(ContentResponse.class);
// when(contentResponse.getContentAsString()).thenReturn("{\"nothing\":\"useful\"}"); no content needed
when(contentResponse.getStatus()).thenReturn(HttpStatus.BAD_REQUEST_400);
Request mockRequest = mock(Request.class);
when(mockRequest.send()).thenReturn(contentResponse);
when(mockRequest.method((HttpMethod) any())).thenReturn(mockRequest);
HttpClient mockHttpClient = spy(HttpClient.class); // spy needed, because some final methods can't be mocked
when(mockHttpClient.newRequest(url)).thenReturn(mockRequest);
fixture = new BridgeDiscoveryParticipant(mockHttpClient);
assertThat(fixture.getPublicInformationFromPossibleBridgeAddress("shcAddress").shcIpAddress, is(""));
}
}

View File

@@ -0,0 +1,236 @@
/**
* 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.discovery;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.ArrayList;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.binding.boschshc.internal.devices.bridge.BridgeHandler;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room;
import org.openhab.core.config.discovery.DiscoveryListener;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingUID;
/**
* ThingDiscoveryService Tester.
*
* @author Gerd Zanker - Initial contribution
*/
@ExtendWith(MockitoExtension.class)
@NonNullByDefault
public class ThingDiscoveryServiceTest {
private @NonNullByDefault({}) ThingDiscoveryService fixture;
private @Mock @NonNullByDefault({}) BridgeHandler bridgeHandler;
private @Mock @NonNullByDefault({}) DiscoveryListener discoveryListener;
private @Captor @NonNullByDefault({}) ArgumentCaptor<DiscoveryService> discoveryServiceCaptor;
private @Captor @NonNullByDefault({}) ArgumentCaptor<DiscoveryResult> discoveryResultCaptor;
@BeforeEach
void beforeEach() {
fixture = new ThingDiscoveryService();
fixture.addDiscoveryListener(discoveryListener);
fixture.setThingHandler(bridgeHandler);
}
private void mockBridgeCalls() {
// Set the Mock Bridge as the ThingHandler
ThingUID bridgeUID = new ThingUID(BoschSHCBindingConstants.THING_TYPE_SHC, "testSHC");
Bridge mockBridge = mock(Bridge.class);
when(mockBridge.getUID()).thenReturn(bridgeUID);
when(bridgeHandler.getThing()).thenReturn(mockBridge);
}
@Test
public void testStartScan() throws InterruptedException {
mockBridgeCalls();
fixture.activate();
fixture.startScan();
verify(bridgeHandler).getRooms();
verify(bridgeHandler).getDevices();
fixture.stopScan();
fixture.deactivate();
}
@Test
public void testStartScanWithoutBridgeHandler() {
mockBridgeCalls();
// No fixture.setThingHandler(bridgeHandler);
fixture.activate();
fixture.startScan();
// bridgeHandler not called, just no exception expected
fixture.stopScan();
fixture.deactivate();
}
@Test
public void testSetGetThingHandler() {
fixture.setThingHandler(bridgeHandler);
assertThat(fixture.getThingHandler(), is(bridgeHandler));
}
@Test
public void testAddDevices() {
mockBridgeCalls();
ArrayList<Device> devices = new ArrayList<>();
ArrayList<Room> emptyRooms = new ArrayList<>();
Device device1 = new Device();
device1.deviceModel = "TWINGUARD";
device1.id = "testDevice:ID";
device1.name = "Test Name";
devices.add(device1);
Device device2 = new Device();
device2.deviceModel = "TWINGUARD";
device2.id = "testDevice:2";
device2.name = "Second device";
devices.add(device2);
verify(discoveryListener, never()).thingDiscovered(any(), any());
fixture.addDevices(devices, emptyRooms);
// two calls for the two devices expected
verify(discoveryListener, times(2)).thingDiscovered(any(), any());
}
@Test
public void testAddDevicesWithNoDevices() {
ArrayList<Device> emptyDevices = new ArrayList<>();
ArrayList<Room> emptyRooms = new ArrayList<>();
verify(discoveryListener, never()).thingDiscovered(any(), any());
fixture.addDevices(emptyDevices, emptyRooms);
// nothing shall be discovered, but also no exception shall be thrown
verify(discoveryListener, never()).thingDiscovered(any(), any());
}
@Test
public void testAddDevice() {
mockBridgeCalls();
Device device = new Device();
device.deviceModel = "TWINGUARD";
device.id = "testDevice:ID";
device.name = "Test Name";
fixture.addDevice(device, "TestRoom");
verify(discoveryListener).thingDiscovered(discoveryServiceCaptor.capture(), discoveryResultCaptor.capture());
assertThat(discoveryServiceCaptor.getValue().getClass(), is(ThingDiscoveryService.class));
DiscoveryResult result = discoveryResultCaptor.getValue();
assertThat(result.getBindingId(), is(BoschSHCBindingConstants.BINDING_ID));
assertThat(result.getThingTypeUID(), is(BoschSHCBindingConstants.THING_TYPE_TWINGUARD));
assertThat(result.getThingUID().getId(), is("testDevice_ID"));
assertThat(result.getBridgeUID().getId(), is("testSHC"));
assertThat(result.getLabel(), is("Test Name"));
assertThat(result.getProperties().get("Location").toString(), is("TestRoom"));
}
@Test
public void testAddDeviceWithNiceNameAndAppendedRoomName() {
assertDeviceNiceName("-RoomClimateControl-", "TestRoom", "Room Climate Control TestRoom");
}
@Test
public void testAddDeviceWithNiceNameWithEmtpyRoomName() {
assertDeviceNiceName("-RoomClimateControl-", "", "Room Climate Control");
}
@Test
public void testAddDeviceWithNiceNameWithoutAppendingRoomName() {
assertDeviceNiceName("-SmokeDetectionSystem-", "TestRoom", "Smoke Detection System");
}
@Test
public void testAddDeviceWithNiceNameWithoutUsualName() {
assertDeviceNiceName("My other device", "TestRoom", "My other device");
}
private void assertDeviceNiceName(String deviceName, String roomName, String expectedNiceName) {
mockBridgeCalls();
Device device = new Device();
device.deviceModel = "TWINGUARD";
device.id = "testDevice:ID";
device.name = deviceName;
fixture.addDevice(device, roomName);
verify(discoveryListener).thingDiscovered(discoveryServiceCaptor.capture(), discoveryResultCaptor.capture());
assertThat(discoveryServiceCaptor.getValue().getClass(), is(ThingDiscoveryService.class));
DiscoveryResult result = discoveryResultCaptor.getValue();
assertThat(result.getLabel(), is(expectedNiceName));
}
@Test
public void testGetRoomForDevice() {
Device device = new Device();
ArrayList<Room> rooms = new ArrayList<>();
Room room1 = new Room();
room1.id = "r1";
room1.name = "Room1";
rooms.add(room1);
Room room2 = new Room();
room2.id = "r2";
room2.name = "Room 2";
rooms.add(room2);
device.roomId = "r1";
assertThat(fixture.getRoomNameForDevice(device, rooms), is("Room1"));
device.roomId = "r2";
assertThat(fixture.getRoomNameForDevice(device, rooms), is("Room 2"));
device.roomId = "unknown";
assertTrue(fixture.getRoomNameForDevice(device, rooms).isEmpty());
}
@Test
public void testGetThingTypeUID() {
Device device = new Device();
device.deviceModel = "invalid";
assertNull(fixture.getThingTypeUID(device));
// just two spot checks
device.deviceModel = "BBL";
assertThat(fixture.getThingTypeUID(device), is(BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL));
device.deviceModel = "TWINGUARD";
assertThat(fixture.getThingTypeUID(device), is(BoschSHCBindingConstants.THING_TYPE_TWINGUARD));
}
}