[boschshc] Add user defined states (#16028)

Signed-off-by: Patrick Gell <patgit023@gmail.com>
This commit is contained in:
Patrick 2023-12-31 00:56:51 +01:00 committed by GitHub
parent af13c9d133
commit d620d261b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1055 additions and 92 deletions

View File

@ -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 | &#9745; | 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 | &#9745; | Switches the User-defined state on or off. |
## Limitations
No major limitation known.

View File

@ -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";
}

View File

@ -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) {

View File

@ -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 <T extends BoschSHCServiceState> String getServiceStateUrl(String serviceName, String deviceId,
Class<T> 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.
* <p>
@ -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);

View File

@ -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<Device> 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<UserDefinedState> 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<ArrayList<UserDefinedState>>() {
}.getType();
List<UserDefinedState> 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<Room> getRooms() throws InterruptedException {
List<Room> 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.
* <p>
@ -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);
String url = localHttpClient.getBoschSmartHomeUrl(String.format("devices/%s", deviceId));
Request request = localHttpClient.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",
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 <T extends BoschSHCServiceState> @Nullable T getState(String deviceId, String stateName, Class<T> stateClass)
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
@Nullable
BoschHttpClient httpClient = this.httpClient;
if (httpClient == null) {
logger.warn("HttpClient not initialized");
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 <T extends BoschSHCServiceState> @Nullable T getState(String endpoint, Class<T> 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 <T extends BoschSHCServiceState> @Nullable Response putState(String deviceId, String serviceName, T state)
throws InterruptedException, TimeoutException, ExecutionException {
@Nullable
BoschHttpClient httpClient = this.httpClient;
if (httpClient == null) {
logger.warn("HttpClient not initialized");
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 <T extends BoschSHCServiceState> @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);
}
}

View File

@ -42,7 +42,6 @@ public class ScenarioHandler {
}
public void triggerScenario(final BoschHttpClient httpClient, final String scenarioName) {
final Scenario[] scenarios;
try {
scenarios = getAvailableScenarios(httpClient);

View File

@ -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:
*
* <pre>
* {
* "@type": "userDefinedState",
* "id": "23d34fa6-382a-444d-8aae-89c706e22158",
* "name": "atHome",
* "state": false
* }
* </pre>
*
* @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;
}
}

View File

@ -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);
}
}

View File

@ -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<Device> devices = shcBridgeHandler.getDevices();
logger.debug("SHC has {} devices", devices.size());
List<UserDefinedState> 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<UserDefinedState> 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<Device> devices, List<Room> rooms) {

View File

@ -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<BoschSHCSe
@Override
public BoschSHCServiceState deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
JsonObject jsonObject = jsonElement.getAsJsonObject();
JsonElement dataType = jsonObject.get("@type");
switch (dataType.getAsString()) {
@ -58,6 +58,13 @@ public class BoschServiceDataDeserializer implements JsonDeserializer<BoschSHCSe
scenario.lastTimeTriggered = jsonObject.get("lastTimeTriggered").getAsString();
return scenario;
}
case "userDefinedState" -> {
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());
}

View File

@ -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 <TState extends BoschSHCServiceState> @Nullable TState fromJson(String json,
Class<TState> 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 <TState extends BoschSHCServiceState> @Nullable TState fromJson(JsonElement json,
Class<TState> 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 extends BoschSHCServiceState> TState getUserDefinedStateOrNull(JsonElement json,
Class<TState> stateClass) {
if (stateClass.isAssignableFrom(UserStateServiceState.class)) {
return BoschSHCServiceState.getUserDefinedStateOrNull(json.getAsString(), stateClass);
}
return null;
}
private static <TState extends BoschSHCServiceState> TState getUserDefinedStateOrNull(String json,
Class<TState> stateClass) {
if (stateClass.isAssignableFrom(UserStateServiceState.class)) {
var state = new UserStateServiceState();
state.setStateFromString(json);
return (TState) state;
}
return null;
}
}

View File

@ -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<UserStateServiceState> {
public UserStateService() {
super("UserDefinedState", UserStateServiceState.class);
}
}

View File

@ -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 + '\'' + '}';
}
}

View File

@ -21,4 +21,10 @@
<description>Unique ID of the device.</description>
</parameter>
</config-description>
<config-description uri="thing-type:boschshc:user-defined-state">
<parameter name="id" type="text" required="true">
<label>State ID</label>
<description>Unique ID of the state.</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -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.

View File

@ -290,6 +290,21 @@
<config-description-ref uri="thing-type:boschshc:device"/>
</thing-type>
<thing-type id="user-defined-state">
<supported-bridge-type-refs>
<bridge-type-ref id="shc"/>
</supported-bridge-type-refs>
<label>User-defined State</label>
<description>A User-defined state.</description>
<channels>
<channel id="user-state" typeId="user-state"/>
</channels>
<config-description-ref uri="thing-type:boschshc:user-defined-state"/>
</thing-type>
<!-- Channels -->
<channel-type id="system-availability">
@ -553,4 +568,10 @@
<description>Name of the scenario to trigger</description>
</channel-type>
<channel-type id="user-state">
<item-type>Switch</item-type>
<label>State</label>
<description>State of user-defined state</description>
</channel-type>
</thing:thing-descriptions>

View File

@ -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);

View File

@ -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<UserDefinedState> 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<UserDefinedState> 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<UserDefinedState> userStates = fixture.getUserStates();
assertTrue(userStates.isEmpty());
}
@AfterEach
void afterEach() throws Exception {
fixture.dispose();

View File

@ -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> 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<LongPollResult> 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 {

View File

@ -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);

View File

@ -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());
}
}

View File

@ -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<UserStateHandler> {
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<String> deviceId = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<UserStateServiceState> 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<Arguments> provideExceptions() {
return Stream.of(Arguments.of(new TimeoutException("test exception")),
Arguments.of(new InterruptedException("test exception")));
}
}

View File

@ -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<UserDefinedState> 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());
}
}

View File

@ -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());
}
}

View File

@ -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<Arguments> 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());
}
}