From: Patrick <54861416+pat-git023@users.noreply.github.com> Date: Sat, 30 Dec 2023 23:56:51 +0000 (+0100) Subject: [boschshc] Add user defined states (#16028) X-Git-Url: https://git.basschouten.com/?a=commitdiff_plain;h=d620d261b72a7b2912d5ccae8682e43437e743a5;p=openhab-addons.git [boschshc] Add user defined states (#16028) Signed-off-by: Patrick Gell --- diff --git a/bundles/org.openhab.binding.boschshc/README.md b/bundles/org.openhab.binding.boschshc/README.md index c17ce7ff80..4b75bb23c2 100644 --- a/bundles/org.openhab.binding.boschshc/README.md +++ b/bundles/org.openhab.binding.boschshc/README.md @@ -19,6 +19,7 @@ Binding for the Bosch Smart Home. - [Intrusion Detection System](#intrusion-detection-system) - [Smart Bulb](#smart-bulb) - [Smoke Detector](#smoke-detector) + - [User-defined States](#user-defined-states) - [Limitations](#limitations) - [Discovery](#discovery) - [Bridge Configuration](#bridge-configuration) @@ -218,6 +219,19 @@ The smoke detector warns you in case of fire. | smoke-check | String | ☑ | State of the smoke check. Also used to request a new smoke check. | +### User-defined States + +User-defined states enable automations to be better adapted to specific needs and everyday situations. +Individual states can be activated/deactivated and can be used as triggers, conditions and actions in automations. + +**Thing Type ID**: `user-defined-state` + + +| Channel Type ID | Item Type | Writable | Description | +|-----------------|-----------| :------: |--------------------------------------------| +| user-state | Switch | ☑ | Switches the User-defined state on or off. | + + ## Limitations No major limitation known. diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java index c520241131..b87a217a30 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java @@ -49,6 +49,8 @@ public class BoschSHCBindingConstants { public static final ThingTypeUID THING_TYPE_SMART_BULB = new ThingTypeUID(BINDING_ID, "smart-bulb"); public static final ThingTypeUID THING_TYPE_SMOKE_DETECTOR = new ThingTypeUID(BINDING_ID, "smoke-detector"); + public static final ThingTypeUID THING_TYPE_USER_DEFINED_STATE = new ThingTypeUID(BINDING_ID, "user-defined-state"); + // List of all Channel IDs // Auto-generated from thing-types.xml via script, don't modify public static final String CHANNEL_SCENARIO_TRIGGERED = "scenario-triggered"; @@ -87,6 +89,8 @@ public class BoschSHCBindingConstants { public static final String CHANNEL_SILENT_MODE = "silent-mode"; public static final String CHANNEL_ILLUMINANCE = "illuminance"; + public static final String CHANNEL_USER_DEFINED_STATE = "user-state"; + // static device/service names public static final String SERVICE_INTRUSION_DETECTION = "intrusionDetectionSystem"; } diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java index 193cb162c0..010a46fc1e 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java @@ -32,6 +32,7 @@ import org.openhab.binding.boschshc.internal.devices.smartbulb.SmartBulbHandler; import org.openhab.binding.boschshc.internal.devices.smokedetector.SmokeDetectorHandler; import org.openhab.binding.boschshc.internal.devices.thermostat.ThermostatHandler; import org.openhab.binding.boschshc.internal.devices.twinguard.TwinguardHandler; +import org.openhab.binding.boschshc.internal.devices.userdefinedstate.UserStateHandler; import org.openhab.binding.boschshc.internal.devices.wallthermostat.WallThermostatHandler; import org.openhab.binding.boschshc.internal.devices.windowcontact.WindowContactHandler; import org.openhab.core.thing.Bridge; @@ -82,7 +83,8 @@ public class BoschSHCHandlerFactory extends BaseThingHandlerFactory { new ThingTypeHandlerMapping(THING_TYPE_INTRUSION_DETECTION_SYSTEM, IntrusionDetectionHandler::new), new ThingTypeHandlerMapping(THING_TYPE_SMART_PLUG_COMPACT, PlugHandler::new), new ThingTypeHandlerMapping(THING_TYPE_SMART_BULB, SmartBulbHandler::new), - new ThingTypeHandlerMapping(THING_TYPE_SMOKE_DETECTOR, SmokeDetectorHandler::new)); + new ThingTypeHandlerMapping(THING_TYPE_SMOKE_DETECTOR, SmokeDetectorHandler::new), + new ThingTypeHandlerMapping(THING_TYPE_USER_DEFINED_STATE, UserStateHandler::new)); @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java index 2e9e612468..70b0151c9f 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java @@ -38,6 +38,8 @@ import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; import org.openhab.binding.boschshc.internal.serialization.GsonUtils; +import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState; +import org.openhab.binding.boschshc.internal.services.userstate.dto.UserStateServiceState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -131,6 +133,15 @@ public class BoschHttpClient extends HttpClient { return this.getBoschSmartHomeUrl(String.format("devices/%s/services/%s/state", deviceId, serviceName)); } + public String getServiceStateUrl(String serviceName, String deviceId, + Class serviceClass) { + if (serviceClass.isAssignableFrom(UserStateServiceState.class)) { + return this.getBoschSmartHomeUrl(String.format("userdefinedstates/%s/state", deviceId)); + } else { + return getServiceStateUrl(serviceName, deviceId); + } + } + /** * Returns a URL to get general information about a service. *

@@ -291,8 +302,13 @@ public class BoschHttpClient extends HttpClient { .timeout(10, TimeUnit.SECONDS); // Set default timeout if (content != null) { - String body = GsonUtils.DEFAULT_GSON_INSTANCE.toJson(content); - logger.trace("create request for {} and content {}", url, content); + final String body; + if (content.getClass().isAssignableFrom(UserStateServiceState.class)) { + body = ((UserStateServiceState) content).getStateAsString(); + } else { + body = GsonUtils.DEFAULT_GSON_INSTANCE.toJson(content); + } + logger.trace("create request for {} and content {}", url, body); request = request.content(new StringContentProvider(body)); } else { logger.trace("create request for {}", url); diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java index 717ad89d37..858fb8916a 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java @@ -41,6 +41,7 @@ import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceDat 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.devices.bridge.dto.Scenario; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState; import org.openhab.binding.boschshc.internal.discovery.ThingDiscoveryService; import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException; @@ -80,6 +81,8 @@ import com.google.gson.reflect.TypeToken; @NonNullByDefault public class BridgeHandler extends BaseBridgeHandler { + private static final String HTTP_CLIENT_NOT_INITIALIZED = "HttpClient not initialized"; + private final Logger logger = LoggerFactory.getLogger(BridgeHandler.class); /** @@ -154,11 +157,11 @@ public class BridgeHandler extends BaseBridgeHandler { } // Instantiate HttpClient with the SslContextFactory - BoschHttpClient httpClient = this.httpClient = new BoschHttpClient(ipAddress, password, factory); + BoschHttpClient localHttpClient = this.httpClient = new BoschHttpClient(ipAddress, password, factory); // Start http client try { - httpClient.start(); + localHttpClient.start(); } catch (Exception e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String.format("Could not create http connection to controller: %s", e.getMessage())); @@ -170,16 +173,16 @@ public class BridgeHandler extends BaseBridgeHandler { // Initialize bridge in the background. // Start initial access the first time - scheduleInitialAccess(httpClient); + scheduleInitialAccess(localHttpClient); } @Override public void dispose() { // Cancel scheduled pairing. @Nullable - ScheduledFuture scheduledPairing = this.scheduledPairing; - if (scheduledPairing != null) { - scheduledPairing.cancel(true); + ScheduledFuture localScheduledPairing = this.scheduledPairing; + if (localScheduledPairing != null) { + localScheduledPairing.cancel(true); this.scheduledPairing = null; } @@ -187,10 +190,10 @@ public class BridgeHandler extends BaseBridgeHandler { this.longPolling.stop(); @Nullable - BoschHttpClient httpClient = this.httpClient; - if (httpClient != null) { + BoschHttpClient localHttpClient = this.httpClient; + if (localHttpClient != null) { try { - httpClient.stop(); + localHttpClient.stop(); } catch (Exception e) { logger.debug("HttpClient failed on bridge disposal: {}", e.getMessage(), e); } @@ -295,16 +298,16 @@ public class BridgeHandler extends BaseBridgeHandler { */ public boolean checkBridgeAccess() throws InterruptedException { @Nullable - BoschHttpClient httpClient = this.httpClient; + BoschHttpClient localHttpClient = this.httpClient; - if (httpClient == null) { + if (localHttpClient == null) { return false; } try { - logger.debug("Sending http request to BoschSHC to check access: {}", httpClient); - String url = httpClient.getBoschSmartHomeUrl("devices"); - ContentResponse contentResponse = httpClient.createRequest(url, GET).send(); + logger.debug("Sending http request to BoschSHC to check access: {}", localHttpClient); + String url = localHttpClient.getBoschSmartHomeUrl("devices"); + ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send(); // check HTTP status code if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) { @@ -327,15 +330,15 @@ public class BridgeHandler extends BaseBridgeHandler { */ public List getDevices() throws InterruptedException { @Nullable - BoschHttpClient httpClient = this.httpClient; - if (httpClient == null) { + BoschHttpClient localHttpClient = this.httpClient; + if (localHttpClient == 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(); + logger.trace("Sending http request to Bosch to request devices: {}", localHttpClient); + String url = localHttpClient.getBoschSmartHomeUrl("devices"); + ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send(); // check HTTP status code if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) { @@ -357,6 +360,39 @@ public class BridgeHandler extends BaseBridgeHandler { } } + public List getUserStates() throws InterruptedException { + @Nullable + BoschHttpClient localHttpClient = this.httpClient; + if (localHttpClient == null) { + return List.of(); + } + + try { + logger.trace("Sending http request to Bosch to request user-defined states: {}", localHttpClient); + String url = localHttpClient.getBoschSmartHomeUrl("userdefinedstates"); + ContentResponse contentResponse = localHttpClient.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 List.of(); + } + + String content = contentResponse.getContentAsString(); + logger.trace("Request devices completed with success: {} - status code: {}", content, + contentResponse.getStatus()); + + Type collectionType = new TypeToken>() { + }.getType(); + List nullableUserStates = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content, + collectionType); + return Optional.ofNullable(nullableUserStates).orElse(Collections.emptyList()); + } catch (TimeoutException | ExecutionException e) { + logger.debug("Request user-defined states failed because of {}!", e.getMessage(), e); + return List.of(); + } + } + /** * Get a list of rooms from the Smart-Home controller * @@ -365,12 +401,12 @@ public class BridgeHandler extends BaseBridgeHandler { public List getRooms() throws InterruptedException { List emptyRooms = new ArrayList<>(); @Nullable - BoschHttpClient httpClient = this.httpClient; - if (httpClient != null) { + BoschHttpClient localHttpClient = this.httpClient; + if (localHttpClient != null) { try { logger.trace("Sending http request to Bosch to request rooms"); - String url = httpClient.getBoschSmartHomeUrl("rooms"); - ContentResponse contentResponse = httpClient.createRequest(url, GET).send(); + String url = localHttpClient.getBoschSmartHomeUrl("rooms"); + ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send(); // check HTTP status code if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) { @@ -426,6 +462,8 @@ public class BridgeHandler extends BaseBridgeHandler { for (BoschSHCServiceState serviceState : result.result) { if (serviceState instanceof DeviceServiceData deviceServiceData) { handleDeviceServiceData(deviceServiceData); + } else if (serviceState instanceof UserDefinedState userDefinedState) { + handleUserDefinedState(userDefinedState); } else if (serviceState instanceof Scenario scenario) { final Channel channel = this.getThing().getChannel(BoschSHCBindingConstants.CHANNEL_SCENARIO_TRIGGERED); if (channel != null && isLinked(channel.getUID())) { @@ -458,6 +496,24 @@ public class BridgeHandler extends BaseBridgeHandler { } } + private void handleUserDefinedState(@Nullable UserDefinedState userDefinedState) { + if (userDefinedState != null) { + JsonElement state = GsonUtils.DEFAULT_GSON_INSTANCE.toJsonTree(userDefinedState.isState()); + + logger.debug("Got update for user-defined state {} with id {}: {}", userDefinedState.getName(), + userDefinedState.getId(), state); + + var stateId = userDefinedState.getId(); + if (stateId == null || state == null) { + return; + } + + logger.debug("Got update for user-defined state {}", userDefinedState); + + forwardStateToHandlers(userDefinedState, state, stateId); + } + } + /** * Extracts the actual state object from the given {@link DeviceServiceData} instance. *

@@ -482,12 +538,18 @@ public class BridgeHandler extends BaseBridgeHandler { /** * Tries to find handlers for the device with the given ID and forwards the received state to the handlers. * - * @param deviceServiceData object representing updates received in long poll results + * @param serviceData object representing updates received in long poll results * @param state the received state object as JSON element * @param updateDeviceId the ID of the device for which the state update was received */ - private void forwardStateToHandlers(DeviceServiceData deviceServiceData, JsonElement state, String updateDeviceId) { + private void forwardStateToHandlers(BoschSHCServiceState serviceData, JsonElement state, String updateDeviceId) { boolean handled = false; + final String serviceId; + if (serviceData instanceof UserDefinedState userState) { + serviceId = userState.getId(); + } else { + serviceId = ((DeviceServiceData) serviceData).id; + } Bridge bridge = this.getThing(); for (Thing childThing : bridge.getThings()) { @@ -502,9 +564,8 @@ public class BridgeHandler extends BaseBridgeHandler { logger.debug("Registered device: {} - looking for {}", deviceId, updateDeviceId); if (deviceId != null && updateDeviceId.equals(deviceId)) { - logger.debug("Found child: {} - calling processUpdate (id: {}) with {}", handler, - deviceServiceData.id, state); - handler.processUpdate(deviceServiceData.id, state); + logger.debug("Found child: {} - calling processUpdate (id: {}) with {}", handler, serviceId, state); + handler.processUpdate(serviceId, state); } } else { logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler); @@ -526,8 +587,8 @@ public class BridgeHandler extends BaseBridgeHandler { private void handleLongPollFailure(Throwable e) { logger.warn("Long polling failed, will try to reconnect", e); @Nullable - BoschHttpClient httpClient = this.httpClient; - if (httpClient == null) { + BoschHttpClient localHttpClient = this.httpClient; + if (localHttpClient == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "@text/offline.long-polling-failed.http-client-null"); return; @@ -535,36 +596,68 @@ public class BridgeHandler extends BaseBridgeHandler { this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, "@text/offline.long-polling-failed.trying-to-reconnect"); - scheduleInitialAccess(httpClient); + scheduleInitialAccess(localHttpClient); } public Device getDeviceInfo(String deviceId) throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException { @Nullable - BoschHttpClient httpClient = this.httpClient; - if (httpClient == null) { + BoschHttpClient localHttpClient = this.httpClient; + if (localHttpClient == null) { throw new BoschSHCException("HTTP client not initialized"); } - String url = httpClient.getBoschSmartHomeUrl(String.format("devices/%s", deviceId)); - Request request = httpClient.createRequest(url, GET); - - return httpClient.sendRequest(request, Device.class, Device::isValid, (Integer statusCode, String content) -> { - JsonRestExceptionResponse errorResponse = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content, - JsonRestExceptionResponse.class); - if (errorResponse != null && JsonRestExceptionResponse.isValid(errorResponse)) { - if (errorResponse.errorCode.equals(JsonRestExceptionResponse.ENTITY_NOT_FOUND)) { - return new BoschSHCException("@text/offline.conf-error.invalid-device-id"); - } else { - return new BoschSHCException( - String.format("Request for info of device %s failed with status code %d and error code %s", + String url = localHttpClient.getBoschSmartHomeUrl(String.format("devices/%s", deviceId)); + Request request = localHttpClient.createRequest(url, GET); + + return localHttpClient.sendRequest(request, Device.class, Device::isValid, + (Integer statusCode, String content) -> { + JsonRestExceptionResponse errorResponse = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content, + JsonRestExceptionResponse.class); + if (errorResponse != null && JsonRestExceptionResponse.isValid(errorResponse)) { + if (errorResponse.errorCode.equals(JsonRestExceptionResponse.ENTITY_NOT_FOUND)) { + return new BoschSHCException("@text/offline.conf-error.invalid-device-id"); + } else { + return new BoschSHCException(String.format( + "Request for info of device %s failed with status code %d and error code %s", deviceId, errorResponse.statusCode, errorResponse.errorCode)); - } - } else { - return new BoschSHCException(String.format("Request for info of device %s failed with status code %d", - deviceId, statusCode)); - } - }); + } + } else { + return new BoschSHCException(String.format( + "Request for info of device %s failed with status code %d", deviceId, statusCode)); + } + }); + } + + public UserDefinedState getUserStateInfo(String stateId) + throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException { + @Nullable + BoschHttpClient locaHttpClient = this.httpClient; + if (locaHttpClient == null) { + throw new BoschSHCException("HTTP client not initialized"); + } + + String url = locaHttpClient.getBoschSmartHomeUrl(String.format("userdefinedstates/%s", stateId)); + Request request = locaHttpClient.createRequest(url, GET); + + return locaHttpClient.sendRequest(request, UserDefinedState.class, UserDefinedState::isValid, + (Integer statusCode, String content) -> { + JsonRestExceptionResponse errorResponse = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content, + JsonRestExceptionResponse.class); + if (errorResponse != null && JsonRestExceptionResponse.isValid(errorResponse)) { + if (errorResponse.errorCode.equals(JsonRestExceptionResponse.ENTITY_NOT_FOUND)) { + return new BoschSHCException("@text/offline.conf-error.invalid-state-id"); + } else { + return new BoschSHCException(String.format( + "Request for info of user-defines state %s failed with status code %d and error code %s", + stateId, errorResponse.statusCode, errorResponse.errorCode)); + } + } else { + return new BoschSHCException( + String.format("Request for info of user-defined state %s failed with status code %d", + stateId, statusCode)); + } + }); } /** @@ -588,15 +681,15 @@ public class BridgeHandler extends BaseBridgeHandler { public @Nullable T getState(String deviceId, String stateName, Class stateClass) throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { @Nullable - BoschHttpClient httpClient = this.httpClient; - if (httpClient == null) { - logger.warn("HttpClient not initialized"); + BoschHttpClient localHttpClient = this.httpClient; + if (localHttpClient == null) { + logger.warn(HTTP_CLIENT_NOT_INITIALIZED); return null; } - String url = httpClient.getServiceStateUrl(stateName, deviceId); + String url = localHttpClient.getServiceStateUrl(stateName, deviceId, stateClass); logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", stateName, deviceId, url); - return getState(httpClient, url, stateClass); + return getState(localHttpClient, url, stateClass); } /** @@ -614,15 +707,15 @@ public class BridgeHandler extends BaseBridgeHandler { public @Nullable T getState(String endpoint, Class stateClass) throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { @Nullable - BoschHttpClient httpClient = this.httpClient; - if (httpClient == null) { - logger.warn("HttpClient not initialized"); + BoschHttpClient localHttpClient = this.httpClient; + if (localHttpClient == null) { + logger.warn(HTTP_CLIENT_NOT_INITIALIZED); return null; } - String url = httpClient.getBoschSmartHomeUrl(endpoint); + String url = localHttpClient.getBoschSmartHomeUrl(endpoint); logger.debug("getState(): Requesting from Bosch: {}", url); - return getState(httpClient, url, stateClass); + return getState(localHttpClient, url, stateClass); } /** @@ -684,15 +777,15 @@ public class BridgeHandler extends BaseBridgeHandler { public @Nullable Response putState(String deviceId, String serviceName, T state) throws InterruptedException, TimeoutException, ExecutionException { @Nullable - BoschHttpClient httpClient = this.httpClient; - if (httpClient == null) { - logger.warn("HttpClient not initialized"); + BoschHttpClient localHttpClient = this.httpClient; + if (localHttpClient == null) { + logger.warn(HTTP_CLIENT_NOT_INITIALIZED); return null; } // Create request - String url = httpClient.getServiceStateUrl(serviceName, deviceId); - Request request = httpClient.createRequest(url, PUT, state); + String url = localHttpClient.getServiceStateUrl(serviceName, deviceId, state.getClass()); + Request request = localHttpClient.createRequest(url, PUT, state); // Send request return request.send(); @@ -726,28 +819,28 @@ public class BridgeHandler extends BaseBridgeHandler { public @Nullable Response postAction(String endpoint, @Nullable T requestBody) throws InterruptedException, TimeoutException, ExecutionException { @Nullable - BoschHttpClient httpClient = this.httpClient; - if (httpClient == null) { - logger.warn("HttpClient not initialized"); + BoschHttpClient localHttpClient = this.httpClient; + if (localHttpClient == null) { + logger.warn(HTTP_CLIENT_NOT_INITIALIZED); return null; } - String url = httpClient.getBoschSmartHomeUrl(endpoint); - Request request = httpClient.createRequest(url, POST, requestBody); + String url = localHttpClient.getBoschSmartHomeUrl(endpoint); + Request request = localHttpClient.createRequest(url, POST, requestBody); return request.send(); } public @Nullable DeviceServiceData getServiceData(String deviceId, String serviceName) throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { @Nullable - BoschHttpClient httpClient = this.httpClient; - if (httpClient == null) { - logger.warn("HttpClient not initialized"); + BoschHttpClient localHttpClient = this.httpClient; + if (localHttpClient == null) { + logger.warn(HTTP_CLIENT_NOT_INITIALIZED); return null; } - String url = httpClient.getServiceUrl(serviceName, deviceId); + String url = localHttpClient.getServiceUrl(serviceName, deviceId); logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", serviceName, deviceId, url); - return getState(httpClient, url, DeviceServiceData.class); + return getState(localHttpClient, url, DeviceServiceData.class); } } diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/ScenarioHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/ScenarioHandler.java index 54a080a8cf..e78fd2fc59 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/ScenarioHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/ScenarioHandler.java @@ -42,7 +42,6 @@ public class ScenarioHandler { } public void triggerScenario(final BoschHttpClient httpClient, final String scenarioName) { - final Scenario[] scenarios; try { scenarios = getAvailableScenarios(httpClient); diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/UserDefinedState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/UserDefinedState.java new file mode 100644 index 0000000000..e66887a549 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/UserDefinedState.java @@ -0,0 +1,76 @@ +/** + * 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 org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState; + +/** + * Represents a single user-defined state defined on the Bosch Smart Home Controller. + * + * Example from Json: + * + *

+ * {
+ * "@type": "userDefinedState",
+ * "id": "23d34fa6-382a-444d-8aae-89c706e22158",
+ * "name": "atHome",
+ * "state": false
+ * }
+ * 
+ * + * @author Patrick Gell - Initial contribution + */ +public class UserDefinedState extends BoschSHCServiceState { + + private String id; + private String name; + private boolean state; + + public UserDefinedState() { + super("UserDefinedState"); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isState() { + return state; + } + + public void setState(boolean state) { + this.state = state; + } + + @Override + public String toString() { + return "UserDefinedState{" + "id='" + id + '\'' + ", name='" + name + '\'' + ", state=" + state + ", type='" + + type + '\'' + '}'; + } + + public static Boolean isValid(UserDefinedState obj) { + return obj != null && obj.id != null; + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/userdefinedstate/UserStateHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/userdefinedstate/UserStateHandler.java new file mode 100644 index 0000000000..52f5064b16 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/userdefinedstate/UserStateHandler.java @@ -0,0 +1,137 @@ +/** + * 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.userdefinedstate; + +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_USER_DEFINED_STATE; + +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.boschshc.internal.devices.BoschSHCConfiguration; +import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler; +import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; +import org.openhab.binding.boschshc.internal.services.userstate.UserStateService; +import org.openhab.binding.boschshc.internal.services.userstate.dto.UserStateServiceState; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonElement; + +/** + * Handler for user defined states + * + * @author Patrick Gell - Initial contribution + * + */ +@NonNullByDefault +public class UserStateHandler extends BoschSHCHandler { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final UserStateService userStateService; + /** + * Bosch SHC configuration loaded from openHAB configuration. + */ + private @Nullable BoschSHCConfiguration config; + + public UserStateHandler(Thing thing) { + super(thing); + + userStateService = new UserStateService(); + } + + @Override + public void initialize() { + var localConfig = this.config = getConfigAs(BoschSHCConfiguration.class); + String stateId = localConfig.id; + if (stateId == null || stateId.isBlank()) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.conf-error.empty-state-id"); + return; + } + + // Try to get state info to make sure the state exists + try { + var bridgeHandler = this.getBridgeHandler(); + var info = bridgeHandler.getUserStateInfo(stateId); + logger.trace("User-defined state initialized:\n{}", info); + } catch (TimeoutException | ExecutionException | BoschSHCException e) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + return; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + return; + } + super.initialize(); + } + + @Override + public @Nullable String getBoschID() { + if (config != null) { + return config.id; + } + + return null; + } + + @Override + protected void initializeServices() throws BoschSHCException { + super.initializeServices(); + + logger.debug("Initializing service for UserStateHandler"); + this.registerService(userStateService, this::updateChannels, List.of(CHANNEL_USER_DEFINED_STATE), true); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + + if (channelUID.getId().equals(CHANNEL_USER_DEFINED_STATE) && (command instanceof OnOffType onOffCommand)) { + updateUserState(channelUID.getThingUID().getId(), onOffCommand); + } + } + + private void updateUserState(String stateId, OnOffType userState) { + UserStateServiceState serviceState = new UserStateServiceState(); + serviceState.setState(userState == OnOffType.ON); + try { + getBridgeHandler().putState(stateId, "", serviceState); + } catch (BoschSHCException | ExecutionException | TimeoutException e) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + String.format("Error while putting user-defined state for %s", stateId)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + String.format("Error while putting user-defined state for %s", stateId)); + } + } + + private void updateChannels(UserStateServiceState userState) { + super.updateState(CHANNEL_USER_DEFINED_STATE, userState.toOnOffType()); + } + + @Override + public void processUpdate(String serviceName, @Nullable JsonElement stateData) { + super.processUpdate("UserDefinedState", stateData); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryService.java index ae83f196ae..e2e005e756 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryService.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryService.java @@ -24,6 +24,7 @@ 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.binding.boschshc.internal.devices.bridge.dto.UserDefinedState; import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResultBuilder; import org.openhab.core.thing.ThingTypeUID; @@ -164,6 +165,8 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements T logger.debug("SHC has {} rooms", rooms.size()); List devices = shcBridgeHandler.getDevices(); logger.debug("SHC has {} devices", devices.size()); + List userStates = shcBridgeHandler.getUserStates(); + logger.debug("SHC has {} user-defined states", userStates.size()); // Write found devices into openhab.log to support manual configuration for (Device d : devices) { @@ -174,8 +177,47 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements T } } } + for (UserDefinedState userState : userStates) { + logger.debug("Found user-defined state: name={} id={} state={}", userState.getName(), userState.getId(), + userState.isState()); + } addDevices(devices, rooms); + addUserStates(userStates); + } + + protected void addUserStates(List userStates) { + for (UserDefinedState userState : userStates) { + addUserState(userState); + } + } + + private void addUserState(UserDefinedState userState) { + // see startScan for the runtime null check of shcBridgeHandler + assert shcBridgeHandler != null; + + logger.trace("Discovering user-defined state {}", userState.getName()); + logger.trace("- details: id {}, state {}", userState.getId(), userState.isState()); + + ThingTypeUID thingTypeUID = new ThingTypeUID(BoschSHCBindingConstants.BINDING_ID, + BoschSHCBindingConstants.THING_TYPE_USER_DEFINED_STATE.getId()); + + logger.trace("- got thingTypeID '{}' for user-defined state '{}'", thingTypeUID.getId(), userState.getName()); + + ThingUID thingUID = new ThingUID(thingTypeUID, shcBridgeHandler.getThing().getUID(), + userState.getId().replace(':', '_')); + + logger.trace("- got thingUID '{}' for user-defined state: '{}'", thingUID, userState); + + DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID) + .withProperty("id", userState.getId()).withLabel(userState.getName()); + + discoveryResult.withBridge(shcBridgeHandler.getThing().getUID()); + + thingDiscovered(discoveryResult.build()); + + logger.debug("Discovered user-defined state '{}' with thingTypeUID={}, thingUID={}, id={}, state={}", + userState.getName(), thingUID, thingTypeUID, userState.getId(), userState.isState()); } protected void addDevices(List devices, List rooms) { diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/serialization/BoschServiceDataDeserializer.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/serialization/BoschServiceDataDeserializer.java index c04d0bd89a..84fe413643 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/serialization/BoschServiceDataDeserializer.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/serialization/BoschServiceDataDeserializer.java @@ -18,6 +18,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData; import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState; import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState; import com.google.gson.JsonDeserializationContext; @@ -39,7 +40,6 @@ public class BoschServiceDataDeserializer implements JsonDeserializer { + var state = new UserDefinedState(); + state.setId(jsonObject.get("id").getAsString()); + state.setName(jsonObject.get("name").getAsString()); + state.setState(jsonObject.get("state").getAsBoolean()); + return state; + } default -> { return new BoschSHCServiceState(dataType.getAsString()); } diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceState.java index 7d906c6717..9144f8d25b 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceState.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceState.java @@ -14,6 +14,7 @@ package org.openhab.binding.boschshc.internal.services.dto; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.boschshc.internal.serialization.GsonUtils; +import org.openhab.binding.boschshc.internal.services.userstate.dto.UserStateServiceState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -62,9 +63,12 @@ public class BoschSHCServiceState { public static @Nullable TState fromJson(String json, Class stateClass) { - var state = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, stateClass); + var state = getUserDefinedStateOrNull(json, stateClass); if (state == null || !state.isValid()) { - return null; + state = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, stateClass); + if (state == null || !state.isValid()) { + return null; + } } return state; @@ -72,11 +76,31 @@ public class BoschSHCServiceState { public static @Nullable TState fromJson(JsonElement json, Class stateClass) { - var state = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, stateClass); + var state = getUserDefinedStateOrNull(json, stateClass); if (state == null || !state.isValid()) { - return null; + state = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, stateClass); + if (state == null || !state.isValid()) { + return null; + } } - return state; } + + private static TState getUserDefinedStateOrNull(JsonElement json, + Class stateClass) { + if (stateClass.isAssignableFrom(UserStateServiceState.class)) { + return BoschSHCServiceState.getUserDefinedStateOrNull(json.getAsString(), stateClass); + } + return null; + } + + private static TState getUserDefinedStateOrNull(String json, + Class stateClass) { + if (stateClass.isAssignableFrom(UserStateServiceState.class)) { + var state = new UserStateServiceState(); + state.setStateFromString(json); + return (TState) state; + } + return null; + } } diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/userstate/UserStateService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/userstate/UserStateService.java new file mode 100644 index 0000000000..7e05e7d91d --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/userstate/UserStateService.java @@ -0,0 +1,30 @@ +/** + * 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.services.userstate; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.services.BoschSHCService; +import org.openhab.binding.boschshc.internal.services.userstate.dto.UserStateServiceState; + +/** + * Service to get and set the state of a user-defined state. + * + * @author Patrick Gell - Initial contribution + */ +@NonNullByDefault +public class UserStateService extends BoschSHCService { + + public UserStateService() { + super("UserDefinedState", UserStateServiceState.class); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/userstate/dto/UserStateServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/userstate/dto/UserStateServiceState.java new file mode 100644 index 0000000000..338d8154e3 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/userstate/dto/UserStateServiceState.java @@ -0,0 +1,59 @@ +/** + * 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.services.userstate.dto; + +import org.eclipse.jdt.annotation.NonNull; +import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState; +import org.openhab.core.library.types.OnOffType; + +/** + * Represents the state of a user-defined state + * + * @author Patrick Gell - Initial contribution + */ +public class UserStateServiceState extends BoschSHCServiceState { + + public UserStateServiceState() { + super("userdefinedstates"); + } + + /** + * Current state + */ + private boolean state; + + public boolean isState() { + return state; + } + + public void setState(boolean state) { + this.state = state; + } + + public void setStateFromString(final String stateStr) { + this.state = Boolean.parseBoolean(stateStr); + } + + public String getStateAsString() { + return Boolean.toString(state); + } + + public @NonNull OnOffType toOnOffType() { + return OnOffType.from(state); + } + + @Override + public String toString() { + return "UserStateServiceState{" + "state=" + state + ", type='" + type + '\'' + '}'; + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/config/configs.xml b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/config/configs.xml index 0dbe7d94a3..dbfa963a8f 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/config/configs.xml +++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/config/configs.xml @@ -21,4 +21,10 @@ Unique ID of the device. + + + + Unique ID of the state. + + diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties index f2eb06fccf..12375ea375 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties +++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties @@ -31,6 +31,8 @@ thing-type.boschshc.thermostat.label = Thermostat thing-type.boschshc.thermostat.description = Radiator thermostat thing-type.boschshc.twinguard.label = Twinguard thing-type.boschshc.twinguard.description = The Twinguard smoke detector warns you in case of fire and constantly monitors the air. +thing-type.boschshc.user-defined-state.label = User-defined State +thing-type.boschshc.user-defined-state.description = A User-defined state. thing-type.boschshc.wall-thermostat.label = Wall Thermostat thing-type.boschshc.wall-thermostat.description = Display of the current room temperature as well as the relative humidity in the room. thing-type.boschshc.window-contact.label = Door/Window Contact @@ -44,6 +46,8 @@ thing-type.config.boschshc.bridge.password.label = System Password thing-type.config.boschshc.bridge.password.description = The system password of the Bosch Smart Home Controller necessary for pairing. thing-type.config.boschshc.device.id.label = Device ID thing-type.config.boschshc.device.id.description = Unique ID of the device. +thing-type.config.boschshc.user-defined-state.id.label = State ID +thing-type.config.boschshc.user-defined-state.id.description = Unique ID of the state. # channel types @@ -130,6 +134,8 @@ channel-type.boschshc.temperature.label = Temperature channel-type.boschshc.temperature.description = Current measured temperature. channel-type.boschshc.trigger-scenario.label = Trigger Scenario channel-type.boschshc.trigger-scenario.description = Name of the scenario to trigger +channel-type.boschshc.user-state.label = State +channel-type.boschshc.user-state.description = State of user-defined state channel-type.boschshc.valve-tappet-position.label = Valve Tappet Position channel-type.boschshc.valve-tappet-position.description = Current open ratio (0 to 100). @@ -146,3 +152,5 @@ offline.long-polling-failed.trying-to-reconnect = Long polling failed, will try 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. +offline.conf-error.empty-state-id = No ID set. +offline.conf-error.invalid-state-id = ID is invalid. diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml index 0f880173c3..83e69c05e5 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml @@ -290,6 +290,21 @@ + + + + + + + A User-defined state. + + + + + + + + @@ -553,4 +568,10 @@ Name of the scenario to trigger + + Switch + + State of user-defined state + + diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java index 390651f554..1ef36e2b98 100644 --- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java @@ -37,6 +37,7 @@ import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult; import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException; import org.openhab.binding.boschshc.internal.services.binaryswitch.dto.BinarySwitchServiceState; +import org.openhab.binding.boschshc.internal.services.userstate.dto.UserStateServiceState; import org.slf4j.Logger; /** @@ -94,6 +95,12 @@ class BoschHttpClientTest { httpClient.getServiceStateUrl("testService", "testDevice")); } + @Test + void getServiceStateUrlForUserState() { + assertEquals("https://127.0.0.1:8444/smarthome/userdefinedstates/testDevice/state", + httpClient.getServiceStateUrl("testService", "testDevice", UserStateServiceState.class)); + } + @Test void isAccessPossible() throws InterruptedException { assertFalse(httpClient.isAccessPossible()); @@ -165,6 +172,15 @@ class BoschHttpClientTest { @Test void createRequestWithObject() { + UserStateServiceState userState = new UserStateServiceState(); + userState.setState(true); + Request request = httpClient.createRequest("https://127.0.0.1", HttpMethod.GET, userState); + assertNotNull(request); + assertEquals("true", StandardCharsets.UTF_8.decode(request.getContent().iterator().next()).toString()); + } + + @Test + void createRequestForUserDefinedState() { BinarySwitchServiceState binarySwitchState = new BinarySwitchServiceState(); binarySwitchState.on = true; Request request = httpClient.createRequest("https://127.0.0.1", HttpMethod.GET, binarySwitchState); diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandlerTest.java index bd71f83b37..592bfa8b14 100644 --- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandlerTest.java +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandlerTest.java @@ -21,7 +21,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.function.BiFunction; @@ -41,7 +43,10 @@ import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceDat import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceTest; import org.openhab.binding.boschshc.internal.devices.bridge.dto.Faults; import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedStateTest; import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; +import org.openhab.binding.boschshc.internal.serialization.GsonUtils; import org.openhab.binding.boschshc.internal.services.binaryswitch.dto.BinarySwitchServiceState; import org.openhab.binding.boschshc.internal.services.intrusion.actions.arm.dto.ArmActionRequest; import org.openhab.binding.boschshc.internal.services.intrusion.dto.AlarmState; @@ -243,6 +248,7 @@ class BridgeHandlerTest { void getDeviceState() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod(); when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod(); + when(httpClient.getServiceStateUrl(anyString(), anyString(), any())).thenCallRealMethod(); when(httpClient.getServiceStateUrl(anyString(), anyString())).thenCallRealMethod(); Request request = mock(Request.class); @@ -405,6 +411,7 @@ class BridgeHandlerTest { when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod(); when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod(); when(httpClient.getServiceStateUrl(anyString(), anyString())).thenCallRealMethod(); + when(httpClient.getServiceStateUrl(anyString(), anyString(), any())).thenCallRealMethod(); Request request = mock(Request.class); when(request.header(anyString(), anyString())).thenReturn(request); @@ -419,6 +426,78 @@ class BridgeHandlerTest { fixture.putState("hdm:ZigBee:f0d1b80000f2a3e9", "BinarySwitch", binarySwitchState); } + @Test + void getUserStateInfo() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { + when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod(); + when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod(); + String stateId = UUID.randomUUID().toString(); + + Request request = mock(Request.class); + when(request.header(anyString(), anyString())).thenReturn(request); + ContentResponse response = mock(ContentResponse.class); + when(response.getStatus()).thenReturn(200); + when(request.send()).thenReturn(response); + when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request); + when(httpClient.sendRequest(same(request), same(UserDefinedState.class), any(), any())) + .thenReturn(UserDefinedStateTest.createTestState(stateId)); + + UserDefinedState userState = fixture.getUserStateInfo(stateId); + assertEquals(stateId, userState.getId()); + } + + @Test + void getUserStates() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { + when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod(); + when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod(); + String stateId = UUID.randomUUID().toString(); + + Request request = mock(Request.class); + when(request.header(anyString(), anyString())).thenReturn(request); + ContentResponse response = mock(ContentResponse.class); + when(response.getStatus()).thenReturn(200); + when(request.send()).thenReturn(response); + when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request); + when(response.getContentAsString()).thenReturn( + GsonUtils.DEFAULT_GSON_INSTANCE.toJson(List.of(UserDefinedStateTest.createTestState(stateId)))); + + List userStates = fixture.getUserStates(); + assertEquals(1, userStates.size()); + } + + @Test + void getUserStatesReturnsEmptyListIfRequestNotSuccessful() + throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { + when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod(); + when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod(); + + Request request = mock(Request.class); + when(request.header(anyString(), anyString())).thenReturn(request); + ContentResponse response = mock(ContentResponse.class); + when(response.getStatus()).thenReturn(401); + when(request.send()).thenReturn(response); + when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request); + + List userStates = fixture.getUserStates(); + assertTrue(userStates.isEmpty()); + } + + @Test + void getUserStatesReturnsEmptyListIfExceptionHappened() + throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { + when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod(); + when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod(); + + Request request = mock(Request.class); + when(request.header(anyString(), anyString())).thenReturn(request); + ContentResponse response = mock(ContentResponse.class); + when(response.getStatus()).thenReturn(401); + when(request.send()).thenThrow(new TimeoutException("text exception")); + when(httpClient.createRequest(anyString(), same(HttpMethod.GET))).thenReturn(request); + + List userStates = fixture.getUserStates(); + assertTrue(userStates.isEmpty()); + } + @AfterEach void afterEach() throws Exception { fixture.dispose(); diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPollingTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPollingTest.java index 2b28b8d068..628607ca9b 100644 --- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPollingTest.java +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPollingTest.java @@ -50,6 +50,7 @@ import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceDat import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult; import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario; import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState; import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException; @@ -249,9 +250,8 @@ class LongPollingTest { } @Test - void startLongPolling_receiveScenario() + void startLongPollingReceiveScenario() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { - // when(httpClient.getBoschSmartHomeUrl(anyString())).thenCallRealMethod(); when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod(); Request subscribeRequest = mock(Request.class); @@ -290,6 +290,47 @@ class LongPollingTest { assertEquals("1693758693032", longPollResultItem.lastTimeTriggered); } + @Test + void startLongPollingReceiveUserDefinedState() + throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { + when(httpClient.getBoschShcUrl(anyString())).thenCallRealMethod(); + + Request subscribeRequest = mock(Request.class); + when(httpClient.createRequest(anyString(), same(HttpMethod.POST), + argThat((JsonRpcRequest r) -> "RE/subscribe".equals(r.method)))).thenReturn(subscribeRequest); + SubscribeResult subscribeResult = new SubscribeResult(); + when(httpClient.sendRequest(any(), same(SubscribeResult.class), any(), any())).thenReturn(subscribeResult); + + Request longPollRequest = mock(Request.class); + when(httpClient.createRequest(anyString(), same(HttpMethod.POST), + argThat((JsonRpcRequest r) -> "RE/longPoll".equals(r.method)))).thenReturn(longPollRequest); + + fixture.start(httpClient); + + ArgumentCaptor completeListener = ArgumentCaptor.forClass(CompleteListener.class); + verify(longPollRequest).send(completeListener.capture()); + + BufferingResponseListener bufferingResponseListener = (BufferingResponseListener) completeListener.getValue(); + + String longPollResultJSON = "{\"result\":[{\"deleted\":false,\"@type\":\"userDefinedState\",\"name\":\"My User state\",\"id\":\"23d34fa6-382a-444d-8aae-89c706e22155\",\"state\":true}],\"jsonrpc\":\"2.0\"}\n"; + Response response = mock(Response.class); + bufferingResponseListener.onContent(response, + ByteBuffer.wrap(longPollResultJSON.getBytes(StandardCharsets.UTF_8))); + + Result result = mock(Result.class); + bufferingResponseListener.onComplete(result); + + ArgumentCaptor longPollResultCaptor = ArgumentCaptor.forClass(LongPollResult.class); + verify(longPollHandler).accept(longPollResultCaptor.capture()); + LongPollResult longPollResult = longPollResultCaptor.getValue(); + assertEquals(1, longPollResult.result.size()); + assertEquals(longPollResult.result.get(0).getClass(), UserDefinedState.class); + UserDefinedState longPollResultItem = (UserDefinedState) longPollResult.result.get(0); + assertEquals("23d34fa6-382a-444d-8aae-89c706e22155", longPollResultItem.getId()); + assertEquals("My User state", longPollResultItem.getName()); + assertTrue(longPollResultItem.isState()); + } + @Test void startSubscriptionFailure() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/ScenarioHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/ScenarioHandlerTest.java index 3035a6f102..9a0ea10639 100644 --- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/ScenarioHandlerTest.java +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/ScenarioHandlerTest.java @@ -62,7 +62,7 @@ class ScenarioHandlerTest { } @Test - void triggerScenario_ShouldSendPOST_ToBoschAPI() throws Exception { + void triggerScenarioShouldSendPOSTToBoschAPI() throws Exception { // GIVEN final var httpClient = mock(BoschHttpClient.class); final var request = mock(Request.class); @@ -85,7 +85,7 @@ class ScenarioHandlerTest { } @Test - void triggerScenario_ShouldNoSendPOST_ToScenarioNameDoesNotExist() throws Exception { + void triggerScenarioShouldNoSendPOSTToScenarioNameDoesNotExist() throws Exception { // GIVEN final var httpClient = mock(BoschHttpClient.class); final var request = mock(Request.class); @@ -106,7 +106,7 @@ class ScenarioHandlerTest { @ParameterizedTest @MethodSource("exceptionData") - void triggerScenario_ShouldNotPanic_IfBoschAPIThrowsException(final Exception exception) throws Exception { + void triggerScenarioShouldNotPanicIfBoschAPIThrowsException(final Exception exception) throws Exception { // GIVEN final var httpClient = mock(BoschHttpClient.class); final var request = mock(Request.class); @@ -126,7 +126,7 @@ class ScenarioHandlerTest { } @Test - void triggerScenario_ShouldNotPanic_IfPOSTIsNotSuccessful() throws Exception { + void triggerScenarioShouldNotPanicIfPOSTIsNotSuccessful() throws Exception { // GIVEN final var httpClient = mock(BoschHttpClient.class); final var request = mock(Request.class); @@ -150,7 +150,7 @@ class ScenarioHandlerTest { @ParameterizedTest @MethodSource("httpExceptionData") - void triggerScenario_ShouldNotPanic_IfPOSTThrowsException(final Exception exception) throws Exception { + void triggerScenarioShouldNotPanicIfPOSTThrowsException(final Exception exception) throws Exception { // GIVEN final var httpClient = mock(BoschHttpClient.class); final var request = mock(Request.class); diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/UserDefinedStateTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/UserDefinedStateTest.java new file mode 100644 index 0000000000..5a204a167b --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/UserDefinedStateTest.java @@ -0,0 +1,60 @@ +/** + * 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.UUID; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for UserDefinedStateTest + * + * @author Patrick Gell - Initial contribution + */ +@NonNullByDefault +public class UserDefinedStateTest { + + public static UserDefinedState createTestState(final String id) { + UserDefinedState userState = new UserDefinedState(); + userState.setId(id); + userState.setState(true); + userState.setName("test user state"); + return userState; + } + + private @NonNullByDefault({}) UserDefinedState fixture; + private final String testId = UUID.randomUUID().toString(); + + @BeforeEach + void beforeEach() { + fixture = createTestState(testId); + } + + @Test + void testIsValid() { + assertTrue(UserDefinedState.isValid(fixture)); + } + + @Test + void testToString() { + assertEquals( + String.format("UserDefinedState{id='%s', name='test user state', state=true, type='UserDefinedState'}", + testId), + fixture.toString()); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/userdefinedstate/UserStateHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/userdefinedstate/UserStateHandlerTest.java new file mode 100644 index 0000000000..e8f3e2b21a --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/userdefinedstate/UserStateHandlerTest.java @@ -0,0 +1,118 @@ +/** + * 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.userdefinedstate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.openhab.binding.boschshc.internal.devices.AbstractBoschSHCHandlerTest; +import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants; +import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; +import org.openhab.binding.boschshc.internal.services.userstate.dto.UserStateServiceState; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingStatusInfo; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; + +/** + * Unit tests for UserStateHandlerTest + * + * @author Patrick Gell - Initial contribution + */ +@NonNullByDefault +class UserStateHandlerTest extends AbstractBoschSHCHandlerTest { + + private final Configuration config = new Configuration(Map.of("id", UUID.randomUUID().toString())); + + @Override + protected UserStateHandler createFixture() { + return new UserStateHandler(getThing()); + } + + @Override + protected ThingTypeUID getThingTypeUID() { + return BoschSHCBindingConstants.THING_TYPE_USER_DEFINED_STATE; + } + + @Override + protected Configuration getConfiguration() { + return config; + } + + @Test + void testHandleCommandSetState() + throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { + var channel = new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_USER_DEFINED_STATE); + getFixture().handleCommand(channel, OnOffType.ON); + + ArgumentCaptor deviceId = ArgumentCaptor.forClass(String.class); + ArgumentCaptor stateClass = ArgumentCaptor.forClass(UserStateServiceState.class); + + verify(getBridgeHandler()).getUserStateInfo(config.get("id").toString()); + verify(getBridgeHandler()).getState(anyString(), anyString(), any()); + verify(getBridgeHandler()).putState(deviceId.capture(), anyString(), stateClass.capture()); + + assertNotNull(deviceId.getValue()); + assertEquals(channel.getThingUID().getId(), deviceId.getValue()); + + assertNotNull(stateClass.getValue()); + assertTrue(stateClass.getValue().isState()); + } + + @ParameterizedTest() + @MethodSource("provideExceptions") + void testHandleCommandSetStateUpdatesThingStatusOnException(Exception mockException) + throws InterruptedException, TimeoutException, ExecutionException { + reset(getCallback()); + lenient().when(getBridgeHandler().putState(anyString(), anyString(), any(UserStateServiceState.class))) + .thenThrow(mockException); + var channel = new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_USER_DEFINED_STATE); + getFixture().handleCommand(channel, OnOffType.ON); + + verify(getCallback()).getBridge(any(ThingUID.class)); + + ThingStatusInfo expectedStatusInfo = new ThingStatusInfo(ThingStatus.OFFLINE, + ThingStatusDetail.COMMUNICATION_ERROR, + String.format("Error while putting user-defined state for %s", channel.getThingUID().getId())); + verify(getCallback()).statusUpdated(same(getThing()), eq(expectedStatusInfo)); + } + + private static Stream provideExceptions() { + return Stream.of(Arguments.of(new TimeoutException("test exception")), + Arguments.of(new InterruptedException("test exception"))); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryServiceTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryServiceTest.java index 204dbd89ec..d114e99a23 100644 --- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryServiceTest.java +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryServiceTest.java @@ -19,6 +19,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; import java.util.ArrayList; +import java.util.UUID; import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.BeforeEach; @@ -32,6 +33,7 @@ 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.binding.boschshc.internal.devices.bridge.dto.UserDefinedState; import org.openhab.core.config.discovery.DiscoveryListener; import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryService; @@ -234,4 +236,29 @@ class ThingDiscoveryServiceTest { device.deviceModel = "TWINGUARD"; assertThat(fixture.getThingTypeUID(device), is(BoschSHCBindingConstants.THING_TYPE_TWINGUARD)); } + + @Test + void testAddUserDefinedStates() { + mockBridgeCalls(); + + ArrayList userStates = new ArrayList<>(); + + UserDefinedState userState1 = new UserDefinedState(); + userState1.setId(UUID.randomUUID().toString()); + userState1.setName("first defined state"); + userState1.setState(true); + UserDefinedState userState2 = new UserDefinedState(); + userState2.setId(UUID.randomUUID().toString()); + userState2.setName("another defined state"); + userState2.setState(false); + userStates.add(userState1); + userStates.add(userState2); + + verify(discoveryListener, never()).thingDiscovered(any(), any()); + + fixture.addUserStates(userStates); + + // two calls for the two devices expected + verify(discoveryListener, times(2)).thingDiscovered(any(), any()); + } } diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceStateTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceStateTest.java index 36b5361830..2c3fc0f86e 100644 --- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceStateTest.java +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceStateTest.java @@ -17,8 +17,10 @@ import static org.junit.jupiter.api.Assertions.*; import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; import org.openhab.binding.boschshc.internal.serialization.GsonUtils; +import org.openhab.binding.boschshc.internal.services.userstate.dto.UserStateServiceState; import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; /** * Test class @@ -79,4 +81,12 @@ class BoschSHCServiceStateTest { TestState2.class); assertNotEquals(null, state2); } + + @Test + void fromJsonReturnsUserStateServiceStateForValidJson() { + var state = BoschSHCServiceState.fromJson(new JsonPrimitive("false"), UserStateServiceState.class); + assertNotEquals(null, state); + assertTrue(state.getClass().isAssignableFrom(UserStateServiceState.class)); + assertFalse(state.isState()); + } } diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/userstate/dto/UserStateServiceStateTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/userstate/dto/UserStateServiceStateTest.java new file mode 100644 index 0000000000..405f21abfc --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/userstate/dto/UserStateServiceStateTest.java @@ -0,0 +1,74 @@ +/** + * 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.services.userstate.dto; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.openhab.core.library.types.OnOffType; + +/** + * Unit tests for UserStateServiceStateTest + * + * @author Patrick Gell - Initial contribution + */ +class UserStateServiceStateTest { + + UserStateServiceState subject; + + @BeforeEach + void setUp() { + subject = new UserStateServiceState(); + } + + @ParameterizedTest + @MethodSource("provideStringsForIsBlank") + void setStateFromStringUpdatesTheState(String inputState, boolean expectedState) { + subject.setStateFromString(inputState); + + assertEquals(expectedState, subject.isState()); + } + + private static Stream provideStringsForIsBlank() { + return Stream.of(Arguments.of("true", true), Arguments.of("false", false), Arguments.of("True", true), + Arguments.of("False", false), Arguments.of("TRUE", true), Arguments.of("FALSE", false), + Arguments.of(null, false), Arguments.of("", false), Arguments.of(" ", false), + Arguments.of("not blank", false)); + } + + @Test + void getStateAsStringReturnsState() { + subject.setState(false); + + assertEquals("false", subject.getStateAsString()); + + subject.setState(true); + assertEquals("true", subject.getStateAsString()); + } + + @Test + void toOnOffTypeReturnsType() { + subject.setState(false); + + assertEquals(OnOffType.OFF, subject.toOnOffType()); + + subject.setState(true); + assertEquals(OnOffType.ON, subject.toOnOffType()); + } +}