[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:
parent
c30893281c
commit
3b08217ff7
bundles/org.openhab.binding.boschshc
README.md
src
main
java/org/openhab/binding/boschshc/internal
devices
discovery
services
resources/OH-INF/i18n
test/java/org/openhab/binding/boschshc/internal
devices/bridge
discovery
@ -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
|
||||
|
||||
@ -239,19 +242,10 @@ 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-03-20 20:30:48.026 [INFO ] [g.discovery.internal.PersistentInbox] - Added new thing 'boschshc:security-camera-eyes:yourBridgeName:hdm_Cameras_XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' to inbox.
|
||||
2023-03-20 20:30:48.026 [INFO ] [g.discovery.internal.PersistentInbox] - Added new thing 'boschshc:smoke-detector:yourBridgeName:hdm_HomeMaticIP_XXXXXXXXXXXXXXXXXXXXXXXX' to inbox.
|
||||
2023-03-20 20:30:48.027 [INFO ] [g.discovery.internal.PersistentInbox] - Added new thing 'boschshc:twinguard:yourBridgeName:hdm_ZigBee_XXXXXXXXXXXXXXXX' to inbox.
|
||||
2023-03-20 20:30:48.028 [INFO ] [g.discovery.internal.PersistentInbox] - Added new thing 'boschshc:smart-bulb:yourBridgeName:hdm_PhilipsHueBridge_HueLight_XXXXXXXXXXXXXXXX-XX_XXXXXXXXXXXX' to inbox.
|
||||
```
|
||||
|
||||
## Thing Configuration
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
45
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/PublicInformation.java
Normal file
45
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/PublicInformation.java
Normal 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;
|
||||
}
|
161
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/BridgeDiscoveryParticipant.java
Normal file
161
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/BridgeDiscoveryParticipant.java
Normal 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();
|
||||
}
|
||||
}
|
253
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryService.java
Normal file
253
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryService.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -27,7 +27,6 @@ public enum SmokeDetectorCheckState {
|
||||
SMOKE_TEST_FAILED;
|
||||
|
||||
public static SmokeDetectorCheckState from(String stateString) {
|
||||
|
||||
try {
|
||||
return SmokeDetectorCheckState.valueOf(stateString);
|
||||
} catch (Exception a) {
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
190
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/discovery/BridgeDiscoveryParticipantTest.java
Normal file
190
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/discovery/BridgeDiscoveryParticipantTest.java
Normal 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(""));
|
||||
}
|
||||
}
|
236
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryServiceTest.java
Normal file
236
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryServiceTest.java
Normal 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));
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user