[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. | | power-consumption | Number:Power | &#9744; | Current power consumption (W) of the device. |
| energy-consumption | Number:Energy | &#9744; | Cumulated energy consumption (Wh) 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. 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. | | purity-rating | String | &#9744; | Rating of current measured purity. |
| air-description | String | &#9744; | Overall description of the air quality. | | air-description | String | &#9744; | Overall description of the air quality. |
| combined-rating | String | &#9744; | Combined rating 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 ### Door/Window Contact
@ -75,6 +77,8 @@ Detects open windows and doors.
| Channel Type ID | Item Type | Writable | Description | | Channel Type ID | Item Type | Writable | Description |
| --------------- | --------- | :------: | ---------------------------- | | --------------- | --------- | :------: | ---------------------------- |
| contact | Contact | &#9744; | Contact state of the device. | | 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 ### 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 | | Channel Type ID | Item Type | Writable | Description |
| --------------- | --------- | :------: | ------------------------------ | | --------------- | --------- | :------: | ------------------------------ |
| latest-motion | DateTime | &#9744; | The date of the latest motion. | | 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 ### Shutter Control
@ -107,6 +113,8 @@ Radiator thermostat
| temperature | Number:Temperature | &#9744; | Current measured temperature. | | temperature | Number:Temperature | &#9744; | Current measured temperature. |
| valve-tappet-position | Number:Dimensionless | &#9744; | Current open ratio of valve tappet (0 to 100). | | 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. | | 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 ### 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. | | temperature | Number:Temperature | &#9744; | Current measured temperature. |
| humidity | Number:Dimensionless | &#9744; | Current measured humidity (0 to 100). | | 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 ### 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_ARM_ACTION = "arm-action";
public static final String CHANNEL_DISARM_ACTION = "disarm-action"; public static final String CHANNEL_DISARM_ACTION = "disarm-action";
public static final String CHANNEL_MUTE_ACTION = "mute-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 // static device/service names
public static final String SERVICE_INTRUSION_DETECTION = "intrusionDetectionSystem"; 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 serviceName Name of service the update came from.
* @param stateData Current state of device service. Serialized as JSON. * @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 // Check services of device to correctly
for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) { for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
BoschSHCService<? extends BoschSHCServiceState> service = deviceService.service; BoschSHCService<? extends BoschSHCServiceState> service = deviceService.service;

View File

@ -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 * see https://apidocs.bosch-smarthome.com/local/index.html
* *
* @param serviceName the name of the service * @param serviceName the name of the service
* @param deviceId the device identifier * @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) { 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));
} }
/** /**

View File

@ -30,7 +30,7 @@ import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler; 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.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.LongPollResult;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room; import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
@ -51,6 +51,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
/** /**
@ -299,52 +300,96 @@ public class BridgeHandler extends BaseBridgeHandler {
/** /**
* Bridge callback handler for the results of long polls. * Bridge callback handler for the results of long polls.
* *
* It will check the result and * It will check the results and
* forward the received to the bosch thing handlers. * forward the received states to the Bosch thing handlers.
* *
* @param result Results from Long Polling * @param result Results from Long Polling
*/ */
private void handleLongPollResult(LongPollResult result) { private void handleLongPollResult(LongPollResult result) {
for (DeviceStatusUpdate update : result.result) { for (DeviceServiceData deviceServiceData : result.result) {
if (update != null && update.state != null) { handleDeviceServiceData(deviceServiceData);
logger.debug("Got update for service {} of type {}: {}", update.id, update.type, update.state); }
}
var updateDeviceId = update.deviceId; /**
if (updateDeviceId == null) { * Processes a single long poll result.
continue; *
} * @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; var updateDeviceId = deviceServiceData.deviceId;
if (updateDeviceId == null || state == null) {
Bridge bridge = this.getThing(); return;
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);
}
} }
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);
} }
} }
@ -470,7 +515,7 @@ public class BridgeHandler extends BaseBridgeHandler {
return null; return null;
} }
String url = httpClient.getServiceUrl(stateName, deviceId); String url = httpClient.getServiceStateUrl(stateName, deviceId);
logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", stateName, deviceId, url); logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", stateName, deviceId, url);
return getState(httpClient, url, stateClass); return getState(httpClient, url, stateClass);
} }
@ -566,7 +611,7 @@ public class BridgeHandler extends BaseBridgeHandler {
} }
// Create request // Create request
String url = httpClient.getServiceUrl(serviceName, deviceId); String url = httpClient.getServiceStateUrl(serviceName, deviceId);
Request request = httpClient.createRequest(url, PUT, state); Request request = httpClient.createRequest(url, PUT, state);
// Send request // Send request
@ -611,4 +656,18 @@ public class BridgeHandler extends BaseBridgeHandler {
Request request = httpClient.createRequest(url, POST, requestBody); Request request = httpClient.createRequest(url, POST, requestBody);
return request.send(); 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; package org.openhab.binding.boschshc.internal.devices.bridge.dto;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.annotations.SerializedName;
/** /**
* Represents a device status update as represented by the Smart Home * 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 Stefan Kästle - Initial contribution
* @author Christian Oeing - refactorings of e.g. server registration * @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. * Url path of the service the update came from.
*/ */
public String path; public String path;
/**
* The type of message.
*/
@SerializedName("@type")
public String type;
/** /**
* Name of service the update came from. * Name of service the update came from.
*/ */
@ -44,15 +39,26 @@ public class DeviceStatusUpdate {
/** /**
* Current state of device. Serialized as JSON. * Current state of device. Serialized as JSON.
*/ */
public JsonElement state; public @Nullable JsonElement state;
/** /**
* Id of device the update is for. * Id of device the update is for.
*/ */
public @Nullable String deviceId; 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 @Override
public String toString() { 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"} * ],"jsonrpc":"2.0"}
*/ */
public ArrayList<DeviceStatusUpdate> result; public ArrayList<DeviceServiceData> result;
public String jsonrpc; public String jsonrpc;
} }

View File

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

View File

@ -12,14 +12,12 @@
*/ */
package org.openhab.binding.boschshc.internal.devices.thermostat; 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.*;
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 java.util.List; import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault; 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.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.childlock.ChildLockService; import org.openhab.binding.boschshc.internal.services.childlock.ChildLockService;
import org.openhab.binding.boschshc.internal.services.childlock.dto.ChildLockServiceState; import org.openhab.binding.boschshc.internal.services.childlock.dto.ChildLockServiceState;
@ -37,7 +35,7 @@ import org.openhab.core.types.Command;
* @author Christian Oeing - Initial contribution * @author Christian Oeing - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public final class ThermostatHandler extends BoschSHCDeviceHandler { public final class ThermostatHandler extends AbstractBatteryPoweredDeviceHandler {
private ChildLockService childLockService; private ChildLockService childLockService;
@ -48,6 +46,8 @@ public final class ThermostatHandler extends BoschSHCDeviceHandler {
@Override @Override
protected void initializeServices() throws BoschSHCException { protected void initializeServices() throws BoschSHCException {
super.initializeServices();
this.createService(TemperatureLevelService::new, this::updateChannels, List.of(CHANNEL_TEMPERATURE)); this.createService(TemperatureLevelService::new, this::updateChannels, List.of(CHANNEL_TEMPERATURE));
this.createService(ValveTappetService::new, this::updateChannels, List.of(CHANNEL_VALVE_TAPPET_POSITION)); this.createService(ValveTappetService::new, this::updateChannels, List.of(CHANNEL_VALVE_TAPPET_POSITION));
this.registerService(this.childLockService, this::updateChannels, List.of(CHANNEL_CHILD_LOCK)); this.registerService(this.childLockService, this::updateChannels, List.of(CHANNEL_CHILD_LOCK));

View File

@ -12,14 +12,7 @@
*/ */
package org.openhab.binding.boschshc.internal.devices.twinguard; 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.*;
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 java.util.List; import java.util.List;
@ -27,7 +20,7 @@ import javax.measure.quantity.Dimensionless;
import javax.measure.quantity.Temperature; import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault; 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.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.airqualitylevel.AirQualityLevelService; import org.openhab.binding.boschshc.internal.services.airqualitylevel.AirQualityLevelService;
import org.openhab.binding.boschshc.internal.services.airqualitylevel.dto.AirQualityLevelServiceState; 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 * @author Christian Oeing - Use service instead of custom logic
*/ */
@NonNullByDefault @NonNullByDefault
public class TwinguardHandler extends BoschSHCDeviceHandler { public class TwinguardHandler extends AbstractBatteryPoweredDeviceHandler {
public TwinguardHandler(Thing thing) { public TwinguardHandler(Thing thing) {
super(thing); super(thing);

View File

@ -12,13 +12,12 @@
*/ */
package org.openhab.binding.boschshc.internal.devices.wallthermostat; 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.*;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE;
import java.util.List; import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault; 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.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.humiditylevel.HumidityLevelService; import org.openhab.binding.boschshc.internal.services.humiditylevel.HumidityLevelService;
import org.openhab.binding.boschshc.internal.services.humiditylevel.dto.HumidityLevelServiceState; import org.openhab.binding.boschshc.internal.services.humiditylevel.dto.HumidityLevelServiceState;
@ -32,7 +31,7 @@ import org.openhab.core.thing.Thing;
* @author Christian Oeing - Initial contribution * @author Christian Oeing - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public final class WallThermostatHandler extends BoschSHCDeviceHandler { public final class WallThermostatHandler extends AbstractBatteryPoweredDeviceHandler {
public WallThermostatHandler(Thing thing) { public WallThermostatHandler(Thing thing) {
super(thing); super(thing);
@ -40,6 +39,8 @@ public final class WallThermostatHandler extends BoschSHCDeviceHandler {
@Override @Override
protected void initializeServices() throws BoschSHCException { protected void initializeServices() throws BoschSHCException {
super.initializeServices();
this.createService(TemperatureLevelService::new, this::updateChannels, List.of(CHANNEL_TEMPERATURE)); this.createService(TemperatureLevelService::new, this::updateChannels, List.of(CHANNEL_TEMPERATURE));
this.createService(HumidityLevelService::new, this::updateChannels, List.of(CHANNEL_HUMIDITY)); this.createService(HumidityLevelService::new, this::updateChannels, List.of(CHANNEL_HUMIDITY));
} }

View File

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

View File

@ -159,7 +159,7 @@ public abstract class BoschSHCService<TState extends BoschSHCServiceState> exten
* *
* @param stateData Current state of service. Serialized as JSON. * @param stateData Current state of service. Serialized as JSON.
*/ */
public void onStateUpdate(JsonElement stateData) { public void onStateUpdate(@Nullable JsonElement stateData) {
@Nullable @Nullable
TState state = BoschSHCServiceState.fromJson(stateData, this.stateClass); TState state = BoschSHCServiceState.fromJson(stateData, this.stateClass);
if (state == null) { if (state == null) {

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

@ -40,7 +40,7 @@ public class BoschSHCServiceState {
private @Nullable String stateType = null; private @Nullable String stateType = null;
@SerializedName("@type") @SerializedName("@type")
private final String type; public final String type;
protected BoschSHCServiceState(String type) { protected BoschSHCServiceState(String type) {
this.type = type; this.type = type;

View File

@ -53,7 +53,7 @@
<bridge-type-ref id="shc"/> <bridge-type-ref id="shc"/>
</supported-bridge-type-refs> </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> <description>The Twinguard smoke detector warns you in case of fire and constantly monitors the air.</description>
<channels> <channels>
@ -65,6 +65,8 @@
<channel id="air-description" typeId="air-description"/> <channel id="air-description" typeId="air-description"/>
<channel id="purity-rating" typeId="purity-rating"/> <channel id="purity-rating" typeId="purity-rating"/>
<channel id="combined-rating" typeId="combined-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> </channels>
<config-description-ref uri="thing-type:boschshc:device"/> <config-description-ref uri="thing-type:boschshc:device"/>
@ -81,6 +83,8 @@
<channels> <channels>
<channel id="contact" typeId="contact"/> <channel id="contact" typeId="contact"/>
<channel id="battery-level" typeId="system.battery-level"/>
<channel id="low-battery" typeId="system.low-battery"/>
</channels> </channels>
<config-description-ref uri="thing-type:boschshc:device"/> <config-description-ref uri="thing-type:boschshc:device"/>
@ -98,6 +102,8 @@
<channels> <channels>
<channel id="latest-motion" typeId="latest-motion"/> <channel id="latest-motion" typeId="latest-motion"/>
<channel id="battery-level" typeId="system.battery-level"/>
<channel id="low-battery" typeId="system.low-battery"/>
</channels> </channels>
<config-description-ref uri="thing-type:boschshc:device"/> <config-description-ref uri="thing-type:boschshc:device"/>
@ -132,6 +138,8 @@
<channel id="temperature" typeId="temperature"/> <channel id="temperature" typeId="temperature"/>
<channel id="valve-tappet-position" typeId="valve-tappet-position"/> <channel id="valve-tappet-position" typeId="valve-tappet-position"/>
<channel id="child-lock" typeId="child-lock"/> <channel id="child-lock" typeId="child-lock"/>
<channel id="battery-level" typeId="system.battery-level"/>
<channel id="low-battery" typeId="system.low-battery"/>
</channels> </channels>
<config-description-ref uri="thing-type:boschshc:device"/> <config-description-ref uri="thing-type:boschshc:device"/>
@ -167,6 +175,8 @@
<channels> <channels>
<channel id="temperature" typeId="temperature"/> <channel id="temperature" typeId="temperature"/>
<channel id="humidity" typeId="humidity"/> <channel id="humidity" typeId="humidity"/>
<channel id="battery-level" typeId="system.battery-level"/>
<channel id="low-battery" typeId="system.low-battery"/>
</channels> </channels>
<config-description-ref uri="thing-type:boschshc:device"/> <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.OnOffType;
import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.QuantityType;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.ThingUID;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
@ -60,8 +59,6 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
public void testHandleCommand() public void testHandleCommand()
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
when(getThing().getUID()).thenReturn(new ThingUID("boschshc", "abcdef"));
getFixture().handleCommand(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), getFixture().handleCommand(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_POWER_SWITCH),
OnOffType.ON); OnOffType.ON);
verify(getBridgeHandler()).putState(eq(getDeviceID()), eq("PowerSwitch"), serviceStateCaptor.capture()); 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); assertSame(PowerSwitchState.OFF, state.switchState);
} }
protected abstract ThingTypeUID getThingTypeUID();
@Test @Test
public void testUpdateChannel_PowerSwitchState() { public void testUpdateChannel_PowerSwitchState() {
when(getThing().getUID()).thenReturn(new ThingUID("boschshc", "abcdef"));
JsonElement jsonObject = JsonParser JsonElement jsonObject = JsonParser
.parseString("{\n" + " \"@type\": \"powerSwitchState\",\n" + " \"switchState\": \"ON\"\n" + "}"); .parseString("{\n" + " \"@type\": \"powerSwitchState\",\n" + " \"switchState\": \"ON\"\n" + "}");
getFixture().processUpdate("PowerSwitch", jsonObject); getFixture().processUpdate("PowerSwitch", jsonObject);
@ -97,8 +90,6 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
@Test @Test
public void testUpdateChannel_PowerMeterServiceState() { public void testUpdateChannel_PowerMeterServiceState() {
when(getThing().getUID()).thenReturn(new ThingUID("boschshc", "abcdef"));
JsonElement jsonObject = JsonParser.parseString("{\n" + " \"@type\": \"powerMeterState\",\n" JsonElement jsonObject = JsonParser.parseString("{\n" + " \"@type\": \"powerMeterState\",\n"
+ " \"powerConsumption\": \"23\",\n" + " \"energyConsumption\": 42\n" + "}"); + " \"powerConsumption\": \"23\",\n" + " \"energyConsumption\": 42\n" + "}");
getFixture().processUpdate("PowerMeter", jsonObject); 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.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo; import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandlerCallback; import org.openhab.core.thing.binding.ThingHandlerCallback;
@ -57,6 +58,7 @@ public abstract class AbstractSHCHandlerTest<T extends BoschSHCHandler> {
@BeforeEach @BeforeEach
public void beforeEach() { public void beforeEach() {
fixture = createFixture(); fixture = createFixture();
lenient().when(thing.getUID()).thenReturn(getThingUID());
when(thing.getBridgeUID()).thenReturn(new ThingUID("boschshc", "shc", "myBridgeUID")); when(thing.getBridgeUID()).thenReturn(new ThingUID("boschshc", "shc", "myBridgeUID"));
when(callback.getBridge(any())).thenReturn(bridge); when(callback.getBridge(any())).thenReturn(bridge);
fixture.setCallback(callback); fixture.setCallback(callback);
@ -72,6 +74,12 @@ public abstract class AbstractSHCHandlerTest<T extends BoschSHCHandler> {
return fixture; return fixture;
} }
protected ThingUID getThingUID() {
return new ThingUID(getThingTypeUID(), "abcdef");
}
protected abstract ThingTypeUID getThingTypeUID();
protected Configuration getConfiguration() { protected Configuration getConfiguration() {
return new Configuration(); return new Configuration();
} }

View File

@ -71,10 +71,16 @@ class BoschHttpClientTest {
@Test @Test
void getServiceUrl() { 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")); 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 @Test
void isAccessPossible() throws InterruptedException { void isAccessPossible() throws InterruptedException {
assertFalse(httpClient.isAccessPossible()); 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());
}
}