- [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)
| 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.
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";
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";
}
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;
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) {
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;
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>
.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);
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;
@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);
/**
}
// 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()));
// 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;
}
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);
}
*/
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()) {
*/
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()) {
}
}
+ 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
*
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()) {
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())) {
}
}
+ 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>
/**
* 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()) {
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);
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;
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));
+ }
+ });
}
/**
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);
}
/**
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);
}
/**
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();
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);
}
}
}
public void triggerScenario(final BoschHttpClient httpClient, final String scenarioName) {
-
final Scenario[] scenarios;
try {
scenarios = getAvailableScenarios(httpClient);
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
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;
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) {
}
}
}
+ 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) {
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;
@Override
public BoschSHCServiceState deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
-
JsonObject jsonObject = jsonElement.getAsJsonObject();
JsonElement dataType = jsonObject.get("@type");
switch (dataType.getAsString()) {
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());
}
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;
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;
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;
+ }
}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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 + '\'' + '}';
+ }
+}
<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>
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
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
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).
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.
<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">
<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>
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;
/**
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());
@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);
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;
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;
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);
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);
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();
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;
}
@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);
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 {
}
@Test
- void triggerScenario_ShouldSendPOST_ToBoschAPI() throws Exception {
+ void triggerScenarioShouldSendPOSTToBoschAPI() throws Exception {
// GIVEN
final var httpClient = mock(BoschHttpClient.class);
final var request = mock(Request.class);
}
@Test
- void triggerScenario_ShouldNoSendPOST_ToScenarioNameDoesNotExist() throws Exception {
+ void triggerScenarioShouldNoSendPOSTToScenarioNameDoesNotExist() throws Exception {
// GIVEN
final var httpClient = mock(BoschHttpClient.class);
final var request = mock(Request.class);
@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);
}
@Test
- void triggerScenario_ShouldNotPanic_IfPOSTIsNotSuccessful() throws Exception {
+ void triggerScenarioShouldNotPanicIfPOSTIsNotSuccessful() throws Exception {
// GIVEN
final var httpClient = mock(BoschHttpClient.class);
final var request = mock(Request.class);
@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);
--- /dev/null
+/**
+ * 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());
+ }
+}
--- /dev/null
+/**
+ * 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")));
+ }
+}
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;
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;
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());
+ }
}
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
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());
+ }
}
--- /dev/null
+/**
+ * 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());
+ }
+}