[boschshc] Support obtaining battery states (#13461) (#13631)

This change adds support for obtaining the battery state for the
following devices:

* Motion Detector
* Thermostat
* Twinguard
* Wall Thermostat
* Window/Door Contact

The following changes were made:

* Add system.battery-level and system.low-battery channels to Motion
Detector, Thermostat, Twinguard, Wall Thermostat and Window/Door Contact
* Add constant for battery-level and low-battery channels in
BoschSHCBindingConstants
* Implement abstract handler and service for battery-powered devices
* Let appropriate devices inherit the abstract implementation
* Add missing super calls in initializeServices() methods
* Rename existing getServiceURL() to getServiceStateURL() in HTTP client
* Add methods to retrieve service states without the suffix "/state" in
the URL
* Rename DeviceStatusUpdate to DeviceServiceData
* Let DeviceServiceData extend BoschSHCServiceState
* Extend DeviceServiceData DTO with model for faults
* Enhance bridge handler: handle updates without state sub-objects,
extract methods to enhance readability
* Add unit tests for all affected devices
* Minor code enhancements
* Update documentation

Signed-off-by: David Pace <dev@davidpace.de>
This commit is contained in:
David Pace 2022-11-11 08:23:48 +01:00 committed by GitHub
parent 49fe49c1a9
commit 49fa349590
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 968 additions and 126 deletions

View File

@ -49,7 +49,7 @@ A compact smart plug with energy monitoring capabilities.
| power-consumption | Number:Power | &#9744; | Current power consumption (W) of the device. |
| energy-consumption | Number:Energy | &#9744; | Cumulated energy consumption (Wh) of the device. |
### Twinguard smoke detector
### Twinguard Smoke Detector
The Twinguard smoke detector warns you in case of fire and constantly monitors the air.
@ -65,6 +65,8 @@ The Twinguard smoke detector warns you in case of fire and constantly monitors t
| purity-rating | String | &#9744; | Rating of current measured purity. |
| air-description | String | &#9744; | Overall description of the air quality. |
| combined-rating | String | &#9744; | Combined rating of the air quality. |
| battery-level | Number | &#9744; | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. |
| low-battery | Switch | &#9744; | Indicates whether the battery is low (`ON`) or OK (`OFF`). |
### Door/Window Contact
@ -75,6 +77,8 @@ Detects open windows and doors.
| Channel Type ID | Item Type | Writable | Description |
| --------------- | --------- | :------: | ---------------------------- |
| contact | Contact | &#9744; | Contact state of the device. |
| battery-level | Number | &#9744; | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. |
| low-battery | Switch | &#9744; | Indicates whether the battery is low (`ON`) or OK (`OFF`). |
### Motion Detector
@ -85,6 +89,8 @@ Detects every movement through an intelligent combination of passive infra-red t
| Channel Type ID | Item Type | Writable | Description |
| --------------- | --------- | :------: | ------------------------------ |
| latest-motion | DateTime | &#9744; | The date of the latest motion. |
| battery-level | Number | &#9744; | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. |
| low-battery | Switch | &#9744; | Indicates whether the battery is low (`ON`) or OK (`OFF`). |
### Shutter Control
@ -107,6 +113,8 @@ Radiator thermostat
| temperature | Number:Temperature | &#9744; | Current measured temperature. |
| valve-tappet-position | Number:Dimensionless | &#9744; | Current open ratio of valve tappet (0 to 100). |
| child-lock | Switch | &#9745; | Indicates if child lock is active. |
| battery-level | Number | &#9744; | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. |
| low-battery | Switch | &#9744; | Indicates whether the battery is low (`ON`) or OK (`OFF`). |
### Climate Control
@ -129,6 +137,8 @@ Display of the current room temperature as well as the relative humidity in the
| --------------- | -------------------- | :------: | ------------------------------------- |
| temperature | Number:Temperature | &#9744; | Current measured temperature. |
| humidity | Number:Dimensionless | &#9744; | Current measured humidity (0 to 100). |
| battery-level | Number | &#9744; | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. |
| low-battery | Switch | &#9744; | Indicates whether the battery is low (`ON`) or OK (`OFF`). |
### Security Camera 360

View File

@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2022 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;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.batterylevel.BatteryLevel;
import org.openhab.binding.boschshc.internal.services.batterylevel.BatteryLevelService;
import org.openhab.core.thing.Thing;
/**
* Abstract implementation for battery-powered devices.
*
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
public class AbstractBatteryPoweredDeviceHandler extends BoschSHCDeviceHandler {
/**
* Service to monitor the battery level of the device
*/
private final BatteryLevelService batteryLevelService;
public AbstractBatteryPoweredDeviceHandler(Thing thing) {
super(thing);
this.batteryLevelService = new BatteryLevelService();
}
@Override
protected void initializeServices() throws BoschSHCException {
super.initializeServices();
registerService(batteryLevelService, this::updateChannels, List.of(CHANNEL_BATTERY_LEVEL, CHANNEL_LOW_BATTERY),
true);
}
private void updateChannels(DeviceServiceData deviceServiceData) {
BatteryLevel batteryLevel = BatteryLevel.fromDeviceServiceData(deviceServiceData);
super.updateState(CHANNEL_BATTERY_LEVEL, batteryLevel.toState());
super.updateState(CHANNEL_LOW_BATTERY, batteryLevel.toLowBatteryState());
}
}

View File

@ -74,6 +74,8 @@ public class BoschSHCBindingConstants {
public static final String CHANNEL_ARM_ACTION = "arm-action";
public static final String CHANNEL_DISARM_ACTION = "disarm-action";
public static final String CHANNEL_MUTE_ACTION = "mute-action";
public static final String CHANNEL_BATTERY_LEVEL = "battery-level";
public static final String CHANNEL_LOW_BATTERY = "low-battery";
// static device/service names
public static final String SERVICE_INTRUSION_DETECTION = "intrusionDetectionSystem";

View File

@ -161,7 +161,7 @@ public abstract class BoschSHCHandler extends BaseThingHandler {
* @param serviceName Name of service the update came from.
* @param stateData Current state of device service. Serialized as JSON.
*/
public void processUpdate(String serviceName, JsonElement stateData) {
public void processUpdate(String serviceName, @Nullable JsonElement stateData) {
// Check services of device to correctly
for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
BoschSHCService<? extends BoschSHCServiceState> service = deviceService.service;

View File

@ -77,7 +77,7 @@ public class BoschHttpClient extends HttpClient {
/**
* Returns the pairing URL for the Bosch SHC clients, using port 8443.
* See https://github.com/BoschSmartHome/bosch-shc-api-docs/blob/master/postman/README.md
*
*
* @return URL for pairing
*/
public String getPairingUrl() {
@ -86,7 +86,7 @@ public class BoschHttpClient extends HttpClient {
/**
* Returns a Bosch SHC URL for the endpoint, using port 8444.
*
*
* @param endpoint a endpoint, see https://apidocs.bosch-smarthome.com/local/index.html
* @return Bosch SHC URL for passed endpoint
*/
@ -96,7 +96,7 @@ public class BoschHttpClient extends HttpClient {
/**
* Returns a SmartHome URL for the endpoint - shortcut of {@link BoschSslUtil::getBoschShcUrl()}
*
*
* @param endpoint a endpoint, see https://apidocs.bosch-smarthome.com/local/index.html
* @return SmartHome URL for passed endpoint
*/
@ -105,15 +105,41 @@ public class BoschHttpClient extends HttpClient {
}
/**
* Returns a device & service URL.
* Returns a URL to get or put a service state.
* <p>
* Example:
*
* <pre>
* https://localhost:8444/smarthome/devices/hdm:ZigBee:000d6f0016d1cdae/services/AirQualityLevel/state
* </pre>
*
* see https://apidocs.bosch-smarthome.com/local/index.html
*
*
* @param serviceName the name of the service
* @param deviceId the device identifier
* @return SmartHome URL for passed endpoint
* @return a URL to get or put a service state
*/
public String getServiceStateUrl(String serviceName, String deviceId) {
return this.getBoschSmartHomeUrl(String.format("devices/%s/services/%s/state", deviceId, serviceName));
}
/**
* Returns a URL to get general information about a service.
* <p>
* Example:
*
* <pre>
* https://localhost:8444/smarthome/devices/hdm:ZigBee:000d6f0016d1cdae/services/BatteryLevel
* </pre>
*
* In some cases this URL has to be used to get the service state, for example for battery levels.
*
* @param serviceName the name of the service
* @param deviceId the device identifier
* @return a URL to retrieve general service information
*/
public String getServiceUrl(String serviceName, String deviceId) {
return this.getBoschSmartHomeUrl(String.format("devices/%s/services/%s/state", deviceId, serviceName));
return this.getBoschSmartHomeUrl(String.format("devices/%s/services/%s", deviceId, serviceName));
}
/**
@ -177,7 +203,7 @@ public class BoschHttpClient extends HttpClient {
/**
* Pairs this client with the Bosch SHC.
* Press pairing button on the Bosch Smart Home Controller!
*
*
* @return true if pairing was successful, otherwise false
* @throws InterruptedException in case of an interrupt
*/
@ -228,7 +254,7 @@ public class BoschHttpClient extends HttpClient {
/**
* Creates a HTTP request.
*
*
* @param url for the HTTP request
* @param method for the HTTP request
* @return created HTTP request instance
@ -239,7 +265,7 @@ public class BoschHttpClient extends HttpClient {
/**
* Creates a HTTP request.
*
*
* @param url for the HTTP request
* @param method for the HTTP request
* @param content for the HTTP request
@ -265,7 +291,7 @@ public class BoschHttpClient extends HttpClient {
/**
* Sends a request and expects a response of the specified type.
*
*
* @param request Request to send
* @param responseContentClass Type of expected response
* @param contentValidator Checks if the parsed response is valid

View File

@ -30,7 +30,7 @@ import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceStatusUpdate;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
@ -51,6 +51,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
/**
@ -299,52 +300,96 @@ public class BridgeHandler extends BaseBridgeHandler {
/**
* Bridge callback handler for the results of long polls.
*
* It will check the result and
* forward the received to the bosch thing handlers.
* It will check the results and
* forward the received states to the Bosch thing handlers.
*
* @param result Results from Long Polling
*/
private void handleLongPollResult(LongPollResult result) {
for (DeviceStatusUpdate update : result.result) {
if (update != null && update.state != null) {
logger.debug("Got update for service {} of type {}: {}", update.id, update.type, update.state);
for (DeviceServiceData deviceServiceData : result.result) {
handleDeviceServiceData(deviceServiceData);
}
}
var updateDeviceId = update.deviceId;
if (updateDeviceId == null) {
continue;
}
/**
* Processes a single long poll result.
*
* @param deviceServiceData object representing a single long poll result
*/
private void handleDeviceServiceData(@Nullable DeviceServiceData deviceServiceData) {
if (deviceServiceData != null) {
JsonElement state = obtainState(deviceServiceData);
logger.debug("Got update for device {}", updateDeviceId);
logger.debug("Got update for service {} of type {}: {}", deviceServiceData.id, deviceServiceData.type,
state);
boolean handled = false;
Bridge bridge = this.getThing();
for (Thing childThing : bridge.getThings()) {
// All children of this should implement BoschSHCHandler
@Nullable
ThingHandler baseHandler = childThing.getHandler();
if (baseHandler != null && baseHandler instanceof BoschSHCHandler) {
BoschSHCHandler handler = (BoschSHCHandler) baseHandler;
@Nullable
String deviceId = handler.getBoschID();
handled = true;
logger.debug("Registered device: {} - looking for {}", deviceId, updateDeviceId);
if (deviceId != null && updateDeviceId.equals(deviceId)) {
logger.debug("Found child: {} - calling processUpdate (id: {}) with {}", handler, update.id,
update.state);
handler.processUpdate(update.id, update.state);
}
} else {
logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler);
}
}
if (!handled) {
logger.debug("Could not find a thing for device ID: {}", updateDeviceId);
}
var updateDeviceId = deviceServiceData.deviceId;
if (updateDeviceId == null || state == null) {
return;
}
logger.debug("Got update for device {}", updateDeviceId);
forwardStateToHandlers(deviceServiceData, state, updateDeviceId);
}
}
/**
* Extracts the actual state object from the given {@link DeviceServiceData} instance.
* <p>
* In some special cases like the <code>BatteryLevel</code> service the {@link DeviceServiceData} object itself
* contains the state.
* In all other cases, the state is contained in a sub-object named <code>state</code>.
*
* @param deviceServiceData the {@link DeviceServiceData} object from which the state should be obtained
* @return the state sub-object or the {@link DeviceServiceData} object itself
*/
@Nullable
private JsonElement obtainState(DeviceServiceData deviceServiceData) {
// the battery level service receives no individual state object but rather requires the DeviceServiceData
// structure
if ("BatteryLevel".equals(deviceServiceData.id)) {
return gson.toJsonTree(deviceServiceData);
}
return deviceServiceData.state;
}
/**
* 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 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) {
boolean handled = false;
Bridge bridge = this.getThing();
for (Thing childThing : bridge.getThings()) {
// All children of this should implement BoschSHCHandler
@Nullable
ThingHandler baseHandler = childThing.getHandler();
if (baseHandler != null && baseHandler instanceof BoschSHCHandler) {
BoschSHCHandler handler = (BoschSHCHandler) baseHandler;
@Nullable
String deviceId = handler.getBoschID();
handled = true;
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);
}
} else {
logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler);
}
}
if (!handled) {
logger.debug("Could not find a thing for device ID: {}", updateDeviceId);
}
}
@ -447,7 +492,7 @@ public class BridgeHandler extends BaseBridgeHandler {
* Query the Bosch Smart Home Controller for the state of the given device.
* <p>
* The URL used for retrieving the state has the following structure:
*
*
* <pre>
* https://{IP}:8444/smarthome/devices/{deviceId}/services/{serviceName}/state
* </pre>
@ -470,14 +515,14 @@ public class BridgeHandler extends BaseBridgeHandler {
return null;
}
String url = httpClient.getServiceUrl(stateName, deviceId);
String url = httpClient.getServiceStateUrl(stateName, deviceId);
logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", stateName, deviceId, url);
return getState(httpClient, url, stateClass);
}
/**
* Queries the Bosch Smart Home Controller for the state using an explicit endpoint.
*
*
* @param <T> Type to which the resulting JSON should be deserialized to
* @param endpoint The destination endpoint part of the URL
* @param stateClass Class to convert the resulting JSON to
@ -503,7 +548,7 @@ public class BridgeHandler extends BaseBridgeHandler {
/**
* Sends a HTTP GET request in order to retrieve a state from the Bosch Smart Home Controller.
*
*
* @param <T> Type to which the resulting JSON should be deserialized to
* @param httpClient HTTP client used for sending the request
* @param url URL at which the state should be retrieved
@ -566,7 +611,7 @@ public class BridgeHandler extends BaseBridgeHandler {
}
// Create request
String url = httpClient.getServiceUrl(serviceName, deviceId);
String url = httpClient.getServiceStateUrl(serviceName, deviceId);
Request request = httpClient.createRequest(url, PUT, state);
// Send request
@ -575,7 +620,7 @@ public class BridgeHandler extends BaseBridgeHandler {
/**
* Sends a HTTP POST request without a request body to the given endpoint.
*
*
* @param endpoint The destination endpoint part of the URL
* @return the HTTP response
* @throws InterruptedException
@ -589,7 +634,7 @@ public class BridgeHandler extends BaseBridgeHandler {
/**
* Sends a HTTP POST request with a request body to the given endpoint.
*
*
* @param <T> Type of the request
* @param endpoint The destination endpoint part of the URL
* @param requestBody object representing the request body to be sent, may be <code>null</code>
@ -611,4 +656,18 @@ public class BridgeHandler extends BaseBridgeHandler {
Request request = httpClient.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");
return null;
}
String url = httpClient.getServiceUrl(serviceName, deviceId);
logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", serviceName, deviceId, url);
return getState(httpClient, url, DeviceServiceData.class);
}
}

View File

@ -13,9 +13,9 @@
package org.openhab.binding.boschshc.internal.devices.bridge.dto;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
import com.google.gson.JsonElement;
import com.google.gson.annotations.SerializedName;
/**
* Represents a device status update as represented by the Smart Home
@ -24,18 +24,13 @@ import com.google.gson.annotations.SerializedName;
* @author Stefan Kästle - Initial contribution
* @author Christian Oeing - refactorings of e.g. server registration
*/
public class DeviceStatusUpdate {
public class DeviceServiceData extends BoschSHCServiceState {
/**
* Url path of the service the update came from.
*/
public String path;
/**
* The type of message.
*/
@SerializedName("@type")
public String type;
/**
* Name of service the update came from.
*/
@ -44,15 +39,26 @@ public class DeviceStatusUpdate {
/**
* Current state of device. Serialized as JSON.
*/
public JsonElement state;
public @Nullable JsonElement state;
/**
* Id of device the update is for.
*/
public @Nullable String deviceId;
/**
* An optional object containing information about device faults.
* <p>
* Example: low battery warnings are stored as faults with category <code>WARNING</code>
*/
public @Nullable Faults faults;
public DeviceServiceData() {
super("DeviceServiceData");
}
@Override
public String toString() {
return this.deviceId + "state: " + this.type;
return this.deviceId + " state: " + this.type;
}
}

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2022 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;
/**
* A fault entry containing a category and a type.
* <p>
* Example JSON:
*
* <pre>
* {
* "type":"LOW_BATTERY",
* "category":"WARNING"
* }
* </pre>
*
* @author David Pace - Initial contribution
*
*/
public class Fault {
public String type;
public String category;
}

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.boschshc.internal.devices.bridge.dto;
import java.util.List;
/**
* A container object for faults that can be contained in {@link DeviceServiceData}.
* <p>
* Example JSON:
*
* <pre>
* "faults": {
* "entries": [
* {
* "type":"LOW_BATTERY",
* "category":"WARNING"
* }
* ]
}
* </pre>
*
* @author David Pace - Initial contribution
*
*/
public class Faults {
public List<Fault> entries;
}

View File

@ -35,6 +35,6 @@ public class LongPollResult {
* ],"jsonrpc":"2.0"}
*/
public ArrayList<DeviceStatusUpdate> result;
public ArrayList<DeviceServiceData> result;
public String jsonrpc;
}

View File

@ -17,7 +17,7 @@ import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConst
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.BoschSHCDeviceHandler;
import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandler;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.latestmotion.LatestMotionService;
import org.openhab.binding.boschshc.internal.services.latestmotion.dto.LatestMotionServiceState;
@ -32,7 +32,7 @@ import org.openhab.core.thing.Thing;
* @author Christian Oeing - Use service instead of custom logic
*/
@NonNullByDefault
public class MotionDetectorHandler extends BoschSHCDeviceHandler {
public class MotionDetectorHandler extends AbstractBatteryPoweredDeviceHandler {
public MotionDetectorHandler(Thing thing) {
super(thing);

View File

@ -12,14 +12,12 @@
*/
package org.openhab.binding.boschshc.internal.devices.thermostat;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_CHILD_LOCK;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_VALVE_TAPPET_POSITION;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.BoschSHCDeviceHandler;
import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandler;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.childlock.ChildLockService;
import org.openhab.binding.boschshc.internal.services.childlock.dto.ChildLockServiceState;
@ -33,11 +31,11 @@ import org.openhab.core.types.Command;
/**
* Handler for a thermostat device.
*
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public final class ThermostatHandler extends BoschSHCDeviceHandler {
public final class ThermostatHandler extends AbstractBatteryPoweredDeviceHandler {
private ChildLockService childLockService;
@ -48,6 +46,8 @@ public final class ThermostatHandler extends BoschSHCDeviceHandler {
@Override
protected void initializeServices() throws BoschSHCException {
super.initializeServices();
this.createService(TemperatureLevelService::new, this::updateChannels, List.of(CHANNEL_TEMPERATURE));
this.createService(ValveTappetService::new, this::updateChannels, List.of(CHANNEL_VALVE_TAPPET_POSITION));
this.registerService(this.childLockService, this::updateChannels, List.of(CHANNEL_CHILD_LOCK));
@ -67,7 +67,7 @@ public final class ThermostatHandler extends BoschSHCDeviceHandler {
/**
* Updates the channels which are linked to the {@link TemperatureLevelService}
* of the device.
*
*
* @param state Current state of {@link TemperatureLevelService}.
*/
private void updateChannels(TemperatureLevelServiceState state) {
@ -77,7 +77,7 @@ public final class ThermostatHandler extends BoschSHCDeviceHandler {
/**
* Updates the channels which are linked to the {@link ValveTappetService} of
* the device.
*
*
* @param state Current state of {@link ValveTappetService}.
*/
private void updateChannels(ValveTappetServiceState state) {
@ -87,7 +87,7 @@ public final class ThermostatHandler extends BoschSHCDeviceHandler {
/**
* Updates the channels which are linked to the {@link ChildLockService} of the
* device.
*
*
* @param state Current state of {@link ChildLockService}.
*/
private void updateChannels(ChildLockServiceState state) {

View File

@ -12,14 +12,7 @@
*/
package org.openhab.binding.boschshc.internal.devices.twinguard;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_AIR_DESCRIPTION;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_COMBINED_RATING;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_HUMIDITY;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_HUMIDITY_RATING;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_PURITY;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_PURITY_RATING;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE_RATING;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*;
import java.util.List;
@ -27,7 +20,7 @@ import javax.measure.quantity.Dimensionless;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.BoschSHCDeviceHandler;
import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandler;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.airqualitylevel.AirQualityLevelService;
import org.openhab.binding.boschshc.internal.services.airqualitylevel.dto.AirQualityLevelServiceState;
@ -44,7 +37,7 @@ import org.openhab.core.thing.Thing;
* @author Christian Oeing - Use service instead of custom logic
*/
@NonNullByDefault
public class TwinguardHandler extends BoschSHCDeviceHandler {
public class TwinguardHandler extends AbstractBatteryPoweredDeviceHandler {
public TwinguardHandler(Thing thing) {
super(thing);

View File

@ -12,13 +12,12 @@
*/
package org.openhab.binding.boschshc.internal.devices.wallthermostat;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_HUMIDITY;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.BoschSHCDeviceHandler;
import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandler;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.humiditylevel.HumidityLevelService;
import org.openhab.binding.boschshc.internal.services.humiditylevel.dto.HumidityLevelServiceState;
@ -28,11 +27,11 @@ import org.openhab.core.thing.Thing;
/**
* Handler for a wall thermostat device.
*
*
* @author Christian Oeing - Initial contribution
*/
@NonNullByDefault
public final class WallThermostatHandler extends BoschSHCDeviceHandler {
public final class WallThermostatHandler extends AbstractBatteryPoweredDeviceHandler {
public WallThermostatHandler(Thing thing) {
super(thing);
@ -40,13 +39,15 @@ public final class WallThermostatHandler extends BoschSHCDeviceHandler {
@Override
protected void initializeServices() throws BoschSHCException {
super.initializeServices();
this.createService(TemperatureLevelService::new, this::updateChannels, List.of(CHANNEL_TEMPERATURE));
this.createService(HumidityLevelService::new, this::updateChannels, List.of(CHANNEL_HUMIDITY));
}
/**
* Updates the channels which are linked to the {@link TemperatureLevelService} of the device.
*
*
* @param state Current state of {@link TemperatureLevelService}.
*/
private void updateChannels(TemperatureLevelServiceState state) {
@ -55,7 +56,7 @@ public final class WallThermostatHandler extends BoschSHCDeviceHandler {
/**
* Updates the channels which are linked to the {@link HumidityLevelService} of the device.
*
*
* @param state Current state of {@link HumidityLevelService}.
*/
private void updateChannels(HumidityLevelServiceState state) {

View File

@ -17,7 +17,7 @@ import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConst
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.BoschSHCDeviceHandler;
import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandler;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactService;
import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactState;
@ -32,7 +32,7 @@ import org.openhab.core.types.State;
* @author Stefan Kästle - Initial contribution
*/
@NonNullByDefault
public class WindowContactHandler extends BoschSHCDeviceHandler {
public class WindowContactHandler extends AbstractBatteryPoweredDeviceHandler {
public WindowContactHandler(Thing thing) {
super(thing);
@ -40,6 +40,8 @@ public class WindowContactHandler extends BoschSHCDeviceHandler {
@Override
protected void initializeServices() throws BoschSHCException {
super.initializeServices();
this.createService(ShutterContactService::new, this::updateChannels, List.of(CHANNEL_CONTACT));
}

View File

@ -32,17 +32,17 @@ import com.google.gson.JsonElement;
* same endpoint.
* <p>
* The endpoints of this service have the following URL structure:
*
*
* <pre>
* https://{IP}:8444/smarthome/devices/{deviceId}/services/{serviceName}/state
* </pre>
*
*
* The HTTP client of the bridge will use <code>GET</code> requests to retrieve the state and <code>PUT</code> requests
* to set the state.
* <p>
* The services of the devices and their official APIs can be found
* <a href="https://apidocs.bosch-smarthome.com/local/">here</a>.
*
*
* @author Christian Oeing - Initial contribution
* @author David Pace - Service abstraction
*/
@ -63,7 +63,7 @@ public abstract class BoschSHCService<TState extends BoschSHCServiceState> exten
/**
* Constructor
*
*
* @param serviceName Unique name of the service.
* @param stateClass State class that this service uses for data transfers
* from/to the device.
@ -75,7 +75,7 @@ public abstract class BoschSHCService<TState extends BoschSHCServiceState> exten
/**
* Initializes the service
*
*
* @param bridgeHandler Bridge to use for communication from/to the device
* @param deviceId Id of device this service is for
* @param stateUpdateListener Function to call when a state update was received
@ -89,7 +89,7 @@ public abstract class BoschSHCService<TState extends BoschSHCServiceState> exten
/**
* Returns the class of the state this service provides.
*
*
* @return Class of the state this service provides.
*/
public Class<TState> getStateClass() {
@ -98,7 +98,7 @@ public abstract class BoschSHCService<TState extends BoschSHCServiceState> exten
/**
* Requests the current state of the service and updates it.
*
*
* @throws ExecutionException
* @throws TimeoutException
* @throws InterruptedException
@ -114,7 +114,7 @@ public abstract class BoschSHCService<TState extends BoschSHCServiceState> exten
/**
* Requests the current state of the device with the specified id.
*
*
* @return Current state of the device.
* @throws ExecutionException
* @throws TimeoutException
@ -136,7 +136,7 @@ public abstract class BoschSHCService<TState extends BoschSHCServiceState> exten
/**
* Sets the state of the device with the specified id.
*
*
* @param state State to set.
* @throws InterruptedException
* @throws ExecutionException
@ -156,10 +156,10 @@ public abstract class BoschSHCService<TState extends BoschSHCServiceState> exten
/**
* A state update was received from the bridge
*
*
* @param stateData Current state of service. Serialized as JSON.
*/
public void onStateUpdate(JsonElement stateData) {
public void onStateUpdate(@Nullable JsonElement stateData) {
@Nullable
TState state = BoschSHCServiceState.fromJson(stateData, this.stateClass);
if (state == null) {
@ -171,7 +171,7 @@ public abstract class BoschSHCService<TState extends BoschSHCServiceState> exten
/**
* A state update was received from the bridge.
*
*
* @param state Current state of service as an instance of the state class.
*/
private void onStateUpdate(TState state) {
@ -184,7 +184,7 @@ public abstract class BoschSHCService<TState extends BoschSHCServiceState> exten
/**
* Allows a service to handle a command and create a new state out of it.
* The new state still has to be set via setState.
*
*
* @param command Command to handle
* @throws BoschSHCException If service can not handle command
*/

View File

@ -0,0 +1,119 @@
/**
* Copyright (c) 2010-2022 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.batterylevel;
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.Fault;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Faults;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* Possible battery levels.
*
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
public enum BatteryLevel {
OK,
LOW_BATTERY,
CRITICAL_LOW,
CRITICALLY_LOW_BATTERY,
NOT_AVAILABLE;
/**
* Derives a battery level by analyzing the fault elements in the given device service data.
* <p>
* Note that no fault elements are present when the battery level is OK.
*
* @param deviceServiceData a device service data model
* @return the derived battery level
*/
public static BatteryLevel fromDeviceServiceData(DeviceServiceData deviceServiceData) {
Faults faults = deviceServiceData.faults;
if (faults == null || faults.entries == null || faults.entries.isEmpty()) {
return OK;
}
for (Fault faultEntry : faults.entries) {
if ("warning".equalsIgnoreCase(faultEntry.category)) {
BatteryLevel batteryLevelState = BatteryLevel.get(faultEntry.type);
if (batteryLevelState != null) {
return batteryLevelState;
}
}
}
return OK;
}
/**
* Returns the corresponding battery level for the given string or <code>null</code> if no state matches.
*
* @param identifier the battery level identifier
*
* @return the matching battery level or <code>null</code>
*/
public static @Nullable BatteryLevel get(String identifier) {
for (BatteryLevel batteryLevelState : values()) {
if (batteryLevelState.toString().equalsIgnoreCase(identifier)) {
return batteryLevelState;
}
}
return null;
}
/**
* Transforms a Bosch-specific battery level to a percentage for the <code>system.battery-level</code> channel.
*
* @return a percentage between 0 and 100 as integer
*/
public State toState() {
switch (this) {
case LOW_BATTERY:
return new DecimalType(10);
case CRITICAL_LOW:
case CRITICALLY_LOW_BATTERY:
return new DecimalType(1);
case NOT_AVAILABLE:
return UnDefType.UNDEF;
default:
return new DecimalType(100);
}
}
/**
* Transforms a Bosch-specific battery level to an <code>ON</code>/<code>OFF</code> state for the
* <code>system.low-battery</code> channel.
* <p>
* If the result is <code>ON</code>, the battery is low; if the result is <code>OFF</code> the battery level is OK.
*
* @return
*/
public OnOffType toLowBatteryState() {
switch (this) {
case LOW_BATTERY:
case CRITICAL_LOW:
case CRITICALLY_LOW_BATTERY:
return OnOffType.ON;
default:
return OnOffType.OFF;
}
}
}

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2022 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.batterylevel;
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.bridge.BridgeHandler;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.BoschSHCService;
/**
* Service to retrieve battery levels.
*
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
public class BatteryLevelService extends BoschSHCService<DeviceServiceData> {
public BatteryLevelService() {
super("BatteryLevel", DeviceServiceData.class);
}
@Override
public @Nullable DeviceServiceData getState()
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
String deviceId = getDeviceId();
if (deviceId == null) {
return null;
}
BridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler == null) {
return null;
}
return bridgeHandler.getServiceData(deviceId, getServiceName());
}
}

View File

@ -22,7 +22,7 @@ import com.google.gson.annotations.SerializedName;
/**
* Base Bosch Smart Home Controller service state.
*
*
* @author Christian Oeing - Initial contribution
*/
public class BoschSHCServiceState {
@ -40,7 +40,7 @@ public class BoschSHCServiceState {
private @Nullable String stateType = null;
@SerializedName("@type")
private final String type;
public final String type;
protected BoschSHCServiceState(String type) {
this.type = type;

View File

@ -53,7 +53,7 @@
<bridge-type-ref id="shc"/>
</supported-bridge-type-refs>
<label>TwinGuard</label>
<label>Twinguard</label>
<description>The Twinguard smoke detector warns you in case of fire and constantly monitors the air.</description>
<channels>
@ -65,6 +65,8 @@
<channel id="air-description" typeId="air-description"/>
<channel id="purity-rating" typeId="purity-rating"/>
<channel id="combined-rating" typeId="combined-rating"/>
<channel id="battery-level" typeId="system.battery-level"/>
<channel id="low-battery" typeId="system.low-battery"/>
</channels>
<config-description-ref uri="thing-type:boschshc:device"/>
@ -81,6 +83,8 @@
<channels>
<channel id="contact" typeId="contact"/>
<channel id="battery-level" typeId="system.battery-level"/>
<channel id="low-battery" typeId="system.low-battery"/>
</channels>
<config-description-ref uri="thing-type:boschshc:device"/>
@ -98,6 +102,8 @@
<channels>
<channel id="latest-motion" typeId="latest-motion"/>
<channel id="battery-level" typeId="system.battery-level"/>
<channel id="low-battery" typeId="system.low-battery"/>
</channels>
<config-description-ref uri="thing-type:boschshc:device"/>
@ -132,6 +138,8 @@
<channel id="temperature" typeId="temperature"/>
<channel id="valve-tappet-position" typeId="valve-tappet-position"/>
<channel id="child-lock" typeId="child-lock"/>
<channel id="battery-level" typeId="system.battery-level"/>
<channel id="low-battery" typeId="system.low-battery"/>
</channels>
<config-description-ref uri="thing-type:boschshc:device"/>
@ -167,6 +175,8 @@
<channels>
<channel id="temperature" typeId="temperature"/>
<channel id="humidity" typeId="humidity"/>
<channel id="battery-level" typeId="system.battery-level"/>
<channel id="low-battery" typeId="system.low-battery"/>
</channels>
<config-description-ref uri="thing-type:boschshc:device"/>

View File

@ -0,0 +1,113 @@
/**
* Copyright (c) 2010-2022 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;
import static org.mockito.Mockito.verify;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.UnDefType;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
/**
* Abstract test implementation for battery-powered devices.
*
* @author David Pace - Initial contribution
*
* @param <T> type of the battery-powered device to be tested
*/
@NonNullByDefault
public abstract class AbstractBatteryPoweredDeviceHandlerTest<T extends AbstractBatteryPoweredDeviceHandler>
extends AbstractBoschSHCDeviceHandlerTest<T> {
@Test
public void testProcessUpdate_BatteryLevel_LowBattery() {
JsonElement deviceServiceData = JsonParser.parseString("{ \n" + " \"@type\":\"DeviceServiceData\",\n"
+ " \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n"
+ " \"id\":\"BatteryLevel\",\n" + " \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\",\n"
+ " \"faults\":{ \n" + " \"entries\":[\n" + " {\n"
+ " \"type\":\"LOW_BATTERY\",\n" + " \"category\":\"WARNING\"\n" + " }\n"
+ " ]\n" + " }\n" + "}");
getFixture().processUpdate("BatteryLevel", deviceServiceData);
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL),
new DecimalType(10));
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.ON);
}
@Test
public void testProcessUpdate_BatteryLevel_CriticalLow() {
JsonElement deviceServiceData = JsonParser.parseString("{ \n" + " \"@type\":\"DeviceServiceData\",\n"
+ " \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n"
+ " \"id\":\"BatteryLevel\",\n" + " \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\",\n"
+ " \"faults\":{ \n" + " \"entries\":[\n" + " {\n"
+ " \"type\":\"CRITICAL_LOW\",\n" + " \"category\":\"WARNING\"\n"
+ " }\n" + " ]\n" + " }\n" + "}");
getFixture().processUpdate("BatteryLevel", deviceServiceData);
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL),
new DecimalType(1));
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.ON);
}
@Test
public void testProcessUpdate_BatteryLevel_CriticallyLowBattery() {
JsonElement deviceServiceData = JsonParser.parseString("{ \n" + " \"@type\":\"DeviceServiceData\",\n"
+ " \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n"
+ " \"id\":\"BatteryLevel\",\n" + " \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\",\n"
+ " \"faults\":{ \n" + " \"entries\":[\n" + " {\n"
+ " \"type\":\"CRITICALLY_LOW_BATTERY\",\n" + " \"category\":\"WARNING\"\n"
+ " }\n" + " ]\n" + " }\n" + "}");
getFixture().processUpdate("BatteryLevel", deviceServiceData);
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL),
new DecimalType(1));
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.ON);
}
@Test
public void testProcessUpdate_BatteryLevel_OK() {
JsonElement deviceServiceData = JsonParser.parseString("{ \n" + " \"@type\":\"DeviceServiceData\",\n"
+ " \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n"
+ " \"id\":\"BatteryLevel\",\n" + " \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\" }");
getFixture().processUpdate("BatteryLevel", deviceServiceData);
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL),
new DecimalType(100));
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.OFF);
}
@Test
public void testProcessUpdate_BatteryLevel_NotAvailable() {
JsonElement deviceServiceData = JsonParser.parseString("{ \n" + " \"@type\":\"DeviceServiceData\",\n"
+ " \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n"
+ " \"id\":\"BatteryLevel\",\n" + " \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\",\n"
+ " \"faults\":{ \n" + " \"entries\":[\n" + " {\n"
+ " \"type\":\"NOT_AVAILABLE\",\n" + " \"category\":\"WARNING\"\n"
+ " }\n" + " ]\n" + " }\n" + "}");
getFixture().processUpdate("BatteryLevel", deviceServiceData);
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL), UnDefType.UNDEF);
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.OFF);
}
}

View File

@ -31,7 +31,6 @@ import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitc
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import com.google.gson.JsonElement;
@ -60,8 +59,6 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
public void testHandleCommand()
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
when(getThing().getUID()).thenReturn(new ThingUID("boschshc", "abcdef"));
getFixture().handleCommand(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_POWER_SWITCH),
OnOffType.ON);
verify(getBridgeHandler()).putState(eq(getDeviceID()), eq("PowerSwitch"), serviceStateCaptor.capture());
@ -76,12 +73,8 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
assertSame(PowerSwitchState.OFF, state.switchState);
}
protected abstract ThingTypeUID getThingTypeUID();
@Test
public void testUpdateChannel_PowerSwitchState() {
when(getThing().getUID()).thenReturn(new ThingUID("boschshc", "abcdef"));
JsonElement jsonObject = JsonParser
.parseString("{\n" + " \"@type\": \"powerSwitchState\",\n" + " \"switchState\": \"ON\"\n" + "}");
getFixture().processUpdate("PowerSwitch", jsonObject);
@ -97,8 +90,6 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
@Test
public void testUpdateChannel_PowerMeterServiceState() {
when(getThing().getUID()).thenReturn(new ThingUID("boschshc", "abcdef"));
JsonElement jsonObject = JsonParser.parseString("{\n" + " \"@type\": \"powerMeterState\",\n"
+ " \"powerConsumption\": \"23\",\n" + " \"energyConsumption\": 42\n" + "}");
getFixture().processUpdate("PowerMeter", jsonObject);

View File

@ -27,6 +27,7 @@ import org.openhab.core.thing.Thing;
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;
import org.openhab.core.thing.binding.ThingHandlerCallback;
@ -57,6 +58,7 @@ public abstract class AbstractSHCHandlerTest<T extends BoschSHCHandler> {
@BeforeEach
public void beforeEach() {
fixture = createFixture();
lenient().when(thing.getUID()).thenReturn(getThingUID());
when(thing.getBridgeUID()).thenReturn(new ThingUID("boschshc", "shc", "myBridgeUID"));
when(callback.getBridge(any())).thenReturn(bridge);
fixture.setCallback(callback);
@ -72,6 +74,12 @@ public abstract class AbstractSHCHandlerTest<T extends BoschSHCHandler> {
return fixture;
}
protected ThingUID getThingUID() {
return new ThingUID(getThingTypeUID(), "abcdef");
}
protected abstract ThingTypeUID getThingTypeUID();
protected Configuration getConfiguration() {
return new Configuration();
}

View File

@ -71,10 +71,16 @@ class BoschHttpClientTest {
@Test
void getServiceUrl() {
assertEquals("https://127.0.0.1:8444/smarthome/devices/testDevice/services/testService/state",
assertEquals("https://127.0.0.1:8444/smarthome/devices/testDevice/services/testService",
httpClient.getServiceUrl("testService", "testDevice"));
}
@Test
void getServiceStateUrl() {
assertEquals("https://127.0.0.1:8444/smarthome/devices/testDevice/services/testService/state",
httpClient.getServiceStateUrl("testService", "testDevice"));
}
@Test
void isAccessPossible() throws InterruptedException {
assertFalse(httpClient.isAccessPossible());

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2022 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.motiondetector;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandlerTest;
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.core.thing.ThingTypeUID;
/**
* Unit Tests for {@link MotionDetectorHandler}.
*
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
public class MotionDetectorHandlerTest extends AbstractBatteryPoweredDeviceHandlerTest<MotionDetectorHandler> {
@Override
protected MotionDetectorHandler createFixture() {
return new MotionDetectorHandler(getThing());
}
@Override
protected String getDeviceID() {
return "hdm:ZigBee:000d6f0012fd2571";
}
@Override
protected ThingTypeUID getThingTypeUID() {
return BoschSHCBindingConstants.THING_TYPE_MOTION_DETECTOR;
}
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2022 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.thermostat;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandlerTest;
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.core.thing.ThingTypeUID;
/**
* Unit Tests for {@link ThermostatHandler}.
*
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
public class ThermostatHandlerTest extends AbstractBatteryPoweredDeviceHandlerTest<ThermostatHandler> {
@Override
protected ThermostatHandler createFixture() {
return new ThermostatHandler(getThing());
}
@Override
protected String getDeviceID() {
return "hdm:ZigBee:000d6f0017f1ace2";
}
@Override
protected ThingTypeUID getThingTypeUID() {
return BoschSHCBindingConstants.THING_TYPE_THERMOSTAT;
}
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2022 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.twinguard;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandlerTest;
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.core.thing.ThingTypeUID;
/**
* Unit Tests for {@link TwinguardHandler}.
*
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
public class TwinguardHandlerTest extends AbstractBatteryPoweredDeviceHandlerTest<TwinguardHandler> {
@Override
protected TwinguardHandler createFixture() {
return new TwinguardHandler(getThing());
}
@Override
protected String getDeviceID() {
return "hdm:ZigBee:000d6f0016d1a193";
}
@Override
protected ThingTypeUID getThingTypeUID() {
return BoschSHCBindingConstants.THING_TYPE_TWINGUARD;
}
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2022 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.wallthermostat;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandlerTest;
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.core.thing.ThingTypeUID;
/**
* Unit Tests for {@link WallThermostatHandler}.
*
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
public class WallThermostatHandlerTest extends AbstractBatteryPoweredDeviceHandlerTest<WallThermostatHandler> {
@Override
protected WallThermostatHandler createFixture() {
return new WallThermostatHandler(getThing());
}
@Override
protected String getDeviceID() {
return "hdm:ZigBee:000d6f0016d1a193";
}
@Override
protected ThingTypeUID getThingTypeUID() {
return BoschSHCBindingConstants.THING_TYPE_WALL_THERMOSTAT;
}
}

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) 2010-2022 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.windowcontact;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandlerTest;
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.core.thing.ThingTypeUID;
/**
* Unit Tests for {@link WindowContactHandler}.
*
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
public class WindowContactHandlerTest extends AbstractBatteryPoweredDeviceHandlerTest<WindowContactHandler> {
@Override
protected WindowContactHandler createFixture() {
return new WindowContactHandler(getThing());
}
@Override
protected String getDeviceID() {
return "hdm:HomeMaticIP:3014D711A000009D545DEB39D";
}
@Override
protected ThingTypeUID getThingTypeUID() {
return BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT;
}
}

View File

@ -0,0 +1,98 @@
/**
* Copyright (c) 2010-2022 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.batterylevel;
import static org.junit.jupiter.api.Assertions.*;
import java.util.ArrayList;
import org.junit.jupiter.api.Test;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Fault;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Faults;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.types.UnDefType;
/**
* Unit tests for {@link BatteryLevel}.
*
* @author David Pace - Initial contribution
*
*/
class BatteryLevelTest {
@Test
void testGet() {
assertSame(BatteryLevel.OK, BatteryLevel.get("OK"));
assertSame(BatteryLevel.OK, BatteryLevel.get("ok"));
assertSame(BatteryLevel.LOW_BATTERY, BatteryLevel.get("LOW_BATTERY"));
assertSame(BatteryLevel.LOW_BATTERY, BatteryLevel.get("low_battery"));
assertSame(BatteryLevel.CRITICAL_LOW, BatteryLevel.get("CRITICAL_LOW"));
assertSame(BatteryLevel.CRITICAL_LOW, BatteryLevel.get("critical_low"));
assertSame(BatteryLevel.CRITICALLY_LOW_BATTERY, BatteryLevel.get("CRITICALLY_LOW_BATTERY"));
assertSame(BatteryLevel.CRITICALLY_LOW_BATTERY, BatteryLevel.get("critically_low_battery"));
assertSame(BatteryLevel.NOT_AVAILABLE, BatteryLevel.get("NOT_AVAILABLE"));
assertSame(BatteryLevel.NOT_AVAILABLE, BatteryLevel.get("not_available"));
assertNull(BatteryLevel.get("foo"));
}
@Test
void testFromDeviceServiceData() {
DeviceServiceData deviceServiceData = new DeviceServiceData();
assertSame(BatteryLevel.OK, BatteryLevel.fromDeviceServiceData(deviceServiceData));
Faults faults = new Faults();
deviceServiceData.faults = faults;
assertSame(BatteryLevel.OK, BatteryLevel.fromDeviceServiceData(deviceServiceData));
ArrayList<Fault> entries = new ArrayList<>();
faults.entries = entries;
assertSame(BatteryLevel.OK, BatteryLevel.fromDeviceServiceData(deviceServiceData));
Fault fault = new Fault();
entries.add(fault);
assertSame(BatteryLevel.OK, BatteryLevel.fromDeviceServiceData(deviceServiceData));
fault.category = "WARNING";
fault.type = "LOW_BATTERY";
assertSame(BatteryLevel.LOW_BATTERY, BatteryLevel.fromDeviceServiceData(deviceServiceData));
fault.type = "CRITICAL_LOW";
assertSame(BatteryLevel.CRITICAL_LOW, BatteryLevel.fromDeviceServiceData(deviceServiceData));
fault.type = "CRITICALLY_LOW_BATTERY";
assertSame(BatteryLevel.CRITICALLY_LOW_BATTERY, BatteryLevel.fromDeviceServiceData(deviceServiceData));
fault.type = "FOO";
assertSame(BatteryLevel.OK, BatteryLevel.fromDeviceServiceData(deviceServiceData));
}
@Test
void testToState() {
assertEquals(new DecimalType(100), BatteryLevel.OK.toState());
assertEquals(new DecimalType(10), BatteryLevel.LOW_BATTERY.toState());
assertEquals(new DecimalType(1), BatteryLevel.CRITICAL_LOW.toState());
assertEquals(new DecimalType(1), BatteryLevel.CRITICALLY_LOW_BATTERY.toState());
assertEquals(UnDefType.UNDEF, BatteryLevel.NOT_AVAILABLE.toState());
}
@Test
void testToLowBatteryState() {
assertEquals(OnOffType.OFF, BatteryLevel.OK.toLowBatteryState());
assertEquals(OnOffType.ON, BatteryLevel.LOW_BATTERY.toLowBatteryState());
assertEquals(OnOffType.ON, BatteryLevel.CRITICAL_LOW.toLowBatteryState());
assertEquals(OnOffType.ON, BatteryLevel.CRITICALLY_LOW_BATTERY.toLowBatteryState());
assertEquals(OnOffType.OFF, BatteryLevel.NOT_AVAILABLE.toLowBatteryState());
}
}