[boschshc] Add support for Light/Shutter Control II (#16400)

* [boschshc] Add support for Shutter Control II (#14562)
* add new channel type for child protection

Signed-off-by: David Pace <dev@davidpace.de>
This commit is contained in:
David Pace 2024-03-31 10:36:43 +02:00 committed by GitHub
parent afc6d949e8
commit b77172c6bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 1670 additions and 154 deletions

View File

@ -1,6 +1,6 @@
# Bosch Smart Home Binding
Binding for the Bosch Smart Home.
Binding for Bosch Smart Home devices.
- [Bosch Smart Home Binding](#bosch-smart-home-binding)
- [Supported Things](#supported-things)
@ -10,8 +10,10 @@ Binding for the Bosch Smart Home.
- [Twinguard Smoke Detector](#twinguard-smoke-detector)
- [Door/Window Contact](#door-window-contact)
- [Door/Window Contact II](#door-window-contact-ii)
- [Light Control II](#light-control-ii)
- [Motion Detector](#motion-detector)
- [Shutter Control](#shutter-control)
- [Shutter Control II](#shutter-control-ii)
- [Thermostat](#thermostat)
- [Climate Control](#climate-control)
- [Wall Thermostat](#wall-thermostat)
@ -114,6 +116,22 @@ Detects open windows and doors and features an additional button.
| bypass | Switch | &#9744; | Indicates whether the device is currently bypassed. Possible values are `ON`,`OFF` and `UNDEF` if the bypass state cannot be determined. |
| signal-strength | Number | &#9744; | Communication quality between the device and the Smart Home Controller. Possible values range between 0 (unknown) and 4 (best signal strength). |
### Light Control II
This thing type is used if Light/Shutter Control II devices are configured as light controls.
**Thing Type ID**: `light-control-2`
| Channel Type ID | Item Type | Writable | Description |
| ------------------ | ------------- | :------: | ------------------------------------------------------------- |
| signal-strength | Number | &#9744; | Communication quality between the device and the Smart Home Controller. Possible values range between 0 (unknown) and 4 (best signal strength). |
| power-consumption | Number:Power | &#9744; | Current power consumption (W) of the device. |
| energy-consumption | Number:Energy | &#9744; | Cumulated energy consumption (Wh) of the device. |
| power-switch-1 | Switch | &#9745; | Switches the light on or off (circuit 1). |
| child-protection-1 | Switch | &#9745; | Indicates whether the child protection is active (circuit 1). |
| power-switch-2 | Switch | &#9745; | Switches the light on or off (circuit 2). |
| child-protection-2 | Switch | &#9745; | Indicates whether the child protection is active (circuit 2). |
### Motion Detector
Detects every movement through an intelligent combination of passive infra-red technology and an additional temperature sensor.
@ -137,6 +155,20 @@ Control of your shutter to take any position you desire.
| --------------- | ------------- | :------: | ---------------------------------------- |
| level | Rollershutter | &#9745; | Current open ratio (0 to 100, Step 0.5). |
### Shutter Control II
This thing type is used if Light/Shutter Control II devices are configured as shutter controls.
**Thing Type ID**: `shutter-control-2`
| Channel Type ID | Item Type | Writable | Description |
| ------------------ | ------------- | :------: | ------------------------------------------------- |
| level | Rollershutter | &#9745; | Current open ratio (0 to 100, Step 0.5). |
| signal-strength | Number | &#9744; | Communication quality between the device and the Smart Home Controller. Possible values range between 0 (unknown) and 4 (best signal strength). |
| child-protection | Switch | &#9745; | Indicates whether the child protection is active. |
| power-consumption | Number:Power | &#9744; | Current power consumption (W) of the device. |
| energy-consumption | Number:Energy | &#9744; | Cumulated energy consumption (Wh) of the device. |
### Thermostat
Radiator thermostat

View File

@ -79,10 +79,10 @@ public class BoschShcCommandExtension extends AbstractConsoleCommandExtension im
*/
List<String> getAllBoschShcServices() {
return List.of("airqualitylevel", "batterylevel", "binaryswitch", "bypass", "cameranotification", "childlock",
"communicationquality", "hsbcoloractuator", "humiditylevel", "illuminance", "intrusion", "keypad",
"latestmotion", "multilevelswitch", "powermeter", "powerswitch", "privacymode", "roomclimatecontrol",
"shuttercontact", "shuttercontrol", "silentmode", "smokedetectorcheck", "temperaturelevel", "userstate",
"valvetappet");
"childprotection", "communicationquality", "hsbcoloractuator", "humiditylevel", "illuminance",
"intrusion", "keypad", "latestmotion", "multilevelswitch", "powermeter", "powerswitch", "privacymode",
"roomclimatecontrol", "shuttercontact", "shuttercontrol", "silentmode", "smokedetectorcheck",
"temperaturelevel", "userstate", "valvetappet");
}
@Override

View File

@ -12,20 +12,16 @@
*/
package org.openhab.binding.boschshc.internal.devices;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_POWER_SWITCH;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.powermeter.PowerMeterService;
import org.openhab.binding.boschshc.internal.services.powermeter.dto.PowerMeterServiceState;
import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchService;
import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState;
import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
@ -61,8 +57,6 @@ public abstract class AbstractPowerSwitchHandler extends BoschSHCDeviceHandler {
super.initializeServices();
this.registerService(this.powerSwitchService, this::updateChannels, List.of(CHANNEL_POWER_SWITCH), true);
this.createService(PowerMeterService::new, this::updateChannels,
List.of(CHANNEL_POWER_CONSUMPTION, CHANNEL_ENERGY_CONSUMPTION), true);
}
@Override
@ -79,19 +73,9 @@ public abstract class AbstractPowerSwitchHandler extends BoschSHCDeviceHandler {
}
/**
* Updates the channels which are linked to the {@link PowerMeterService} of the device.
* Updates the power switch channel when a new state is received.
*
* @param state Current state of {@link PowerMeterService}.
*/
private void updateChannels(PowerMeterServiceState state) {
super.updateState(CHANNEL_POWER_CONSUMPTION, new QuantityType<>(state.powerConsumption, Units.WATT));
super.updateState(CHANNEL_ENERGY_CONSUMPTION, new QuantityType<>(state.energyConsumption, Units.WATT_HOUR));
}
/**
* Updates the channels which are linked to the {@link PowerSwitchService} of the device.
*
* @param state Current state of {@link PowerSwitchService}.
* @param state the new {@link PowerSwitchService} state.
*/
private void updateChannels(PowerSwitchServiceState state) {
State powerState = OnOffType.from(state.switchState.toString());

View File

@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2024 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.CHANNEL_ENERGY_CONSUMPTION;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.powermeter.PowerMeterService;
import org.openhab.binding.boschshc.internal.services.powermeter.dto.PowerMeterServiceState;
import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchService;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.Thing;
/**
* Abstract handler implementation for devices providing a {@link PowerSwitchService} and a {@link PowerMeterService}.
* <p>
* Examples for such devices are smart plugs and in-wall switches.
*
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
public abstract class AbstractPowerSwitchHandlerWithPowerMeter extends AbstractPowerSwitchHandler {
protected AbstractPowerSwitchHandlerWithPowerMeter(Thing thing) {
super(thing);
}
@Override
protected void initializeServices() throws BoschSHCException {
super.initializeServices();
this.createService(PowerMeterService::new, this::updateChannels,
List.of(CHANNEL_POWER_CONSUMPTION, CHANNEL_ENERGY_CONSUMPTION), true);
}
/**
* Updates the channels which are linked to the {@link PowerMeterService} of the device.
*
* @param state Current state of {@link PowerMeterService}.
*/
private void updateChannels(PowerMeterServiceState state) {
super.updateState(CHANNEL_POWER_CONSUMPTION, new QuantityType<>(state.powerConsumption, Units.WATT));
super.updateState(CHANNEL_ENERGY_CONSUMPTION, new QuantityType<>(state.energyConsumption, Units.WATT_HOUR));
}
}

View File

@ -0,0 +1,69 @@
/**
* Copyright (c) 2010-2024 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 org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Utilities for handling parent/child relations in Bosch device IDs.
*
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
public final class BoschDeviceIdUtils {
private static final String CHILD_ID_SEPARATOR = "#";
private BoschDeviceIdUtils() {
// Utility Class
}
/**
* Returns whether the given device ID is a child device ID.
* <p>
* Example for a parent device ID:
*
* <pre>
* hdm:ZigBee:70ac08fffefead2d
* </pre>
*
* Example for a child device ID:
*
* <pre>
* hdm:ZigBee:70ac08fffefead2d#2
* </pre>
*
* @param deviceId the Bosch device ID to check
* @return <code>true</code> if the device ID contains a hash character, <code>false</code> otherwise
*/
public static boolean isChildDeviceId(String deviceId) {
return deviceId.contains(CHILD_ID_SEPARATOR);
}
/**
* If the given device ID is a child device ID, the parent device ID is derived by cutting off the part starting
* from the hash character.
*
* @param deviceId a device ID
* @return the parent device ID, if derivable. Otherwise the given ID is returned.
*/
public static String getParentDeviceId(String deviceId) {
int hashIndex = deviceId.indexOf(CHILD_ID_SEPARATOR);
if (hashIndex < 0) {
return deviceId;
}
return deviceId.substring(0, hashIndex);
}
}

View File

@ -39,6 +39,7 @@ public class BoschSHCBindingConstants {
public static final ThingTypeUID THING_TYPE_WINDOW_CONTACT_2 = new ThingTypeUID(BINDING_ID, "window-contact-2");
public static final ThingTypeUID THING_TYPE_MOTION_DETECTOR = new ThingTypeUID(BINDING_ID, "motion-detector");
public static final ThingTypeUID THING_TYPE_SHUTTER_CONTROL = new ThingTypeUID(BINDING_ID, "shutter-control");
public static final ThingTypeUID THING_TYPE_SHUTTER_CONTROL_2 = new ThingTypeUID(BINDING_ID, "shutter-control-2");
public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat");
public static final ThingTypeUID THING_TYPE_CLIMATE_CONTROL = new ThingTypeUID(BINDING_ID, "climate-control");
public static final ThingTypeUID THING_TYPE_WALL_THERMOSTAT = new ThingTypeUID(BINDING_ID, "wall-thermostat");
@ -51,9 +52,10 @@ public class BoschSHCBindingConstants {
public static final ThingTypeUID THING_TYPE_SMOKE_DETECTOR = new ThingTypeUID(BINDING_ID, "smoke-detector");
public static final ThingTypeUID THING_TYPE_UNIVERSAL_SWITCH = new ThingTypeUID(BINDING_ID, "universal-switch");
public static final ThingTypeUID THING_TYPE_UNIVERSAL_SWITCH_2 = new ThingTypeUID(BINDING_ID, "universal-switch-2");
public static final ThingTypeUID THING_TYPE_SMOKE_DETECTOR_2 = new ThingTypeUID(BINDING_ID, "smoke-detector-2");
public static final ThingTypeUID THING_TYPE_LIGHT_CONTROL_2 = new ThingTypeUID(BINDING_ID, "light-control-2");
public static final ThingTypeUID THING_TYPE_USER_DEFINED_STATE = new ThingTypeUID(BINDING_ID, "user-defined-state");
public static final ThingTypeUID THING_TYPE_SMOKE_DETECTOR_2 = new ThingTypeUID(BINDING_ID, "smoke-detector-2");
// List of all Channel IDs
// Auto-generated from thing-types.xml via script, don't modify
@ -76,6 +78,7 @@ public class BoschSHCBindingConstants {
public static final String CHANNEL_VALVE_TAPPET_POSITION = "valve-tappet-position";
public static final String CHANNEL_SETPOINT_TEMPERATURE = "setpoint-temperature";
public static final String CHANNEL_CHILD_LOCK = "child-lock";
public static final String CHANNEL_CHILD_PROTECTION = "child-protection";
public static final String CHANNEL_PRIVACY_MODE = "privacy-mode";
public static final String CHANNEL_CAMERA_NOTIFICATION = "camera-notification";
public static final String CHANNEL_SYSTEM_AVAILABILITY = "system-availability";
@ -99,6 +102,14 @@ public class BoschSHCBindingConstants {
public static final String CHANNEL_KEY_EVENT_TYPE = "key-event-type";
public static final String CHANNEL_KEY_EVENT_TIMESTAMP = "key-event-timestamp";
// numbered channels
// the rationale for introducing numbered channels was discussed in
// https://github.com/openhab/openhab-addons/pull/16400
public static final String CHANNEL_POWER_SWITCH_1 = "power-switch-1";
public static final String CHANNEL_POWER_SWITCH_2 = "power-switch-2";
public static final String CHANNEL_CHILD_PROTECTION_1 = "child-protection-1";
public static final String CHANNEL_CHILD_PROTECTION_2 = "child-protection-2";
public static final String CHANNEL_USER_DEFINED_STATE = "user-state";
// static device/service names

View File

@ -17,6 +17,7 @@ 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.dto.Device;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
@ -57,30 +58,68 @@ public abstract class BoschSHCDeviceHandler extends BoschSHCHandler {
@Override
public void initialize() {
var config = this.config = getConfigAs(BoschSHCConfiguration.class);
this.config = getConfigAs(BoschSHCConfiguration.class);
String deviceId = config.id;
@Nullable
Device deviceInfo = validateDeviceId(deviceId);
if (deviceInfo == null) {
return;
}
if (!processDeviceInfo(deviceInfo)) {
return;
}
super.initialize();
}
/**
* Allows the handler to process the device info that was obtained from a REST
* call to the Smart Home Controller at <code>/devices/{deviceId}</code>.
*
* @param deviceInfo the device info obtained from the controller, guaranteed to be non-null
* @return <code>true</code> if the device info is valid and the initialization should proceed, <code>false</code>
* otherwise
*/
protected boolean processDeviceInfo(Device deviceInfo) {
return true;
}
/**
* Attempts to obtain information about the device with the specified ID via a REST call.
* <p>
* If the REST call is successful, the device ID is considered to be valid and the resulting {@link Device} object
* is returned.
* <p>
* If the device ID is not configured/empty or the REST call is not successful, the device ID is considered invalid
* and <code>null</code> is returned.
*
* @param deviceId the device ID to check
* @return the {@link Device} info object if the REST call was successful, <code>null</code> otherwise
*/
@Nullable
protected Device validateDeviceId(@Nullable String deviceId) {
if (deviceId == null || deviceId.isBlank()) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.conf-error.empty-device-id");
return;
return null;
}
// Try to get device info to make sure the device exists
try {
var bridgeHandler = this.getBridgeHandler();
var info = bridgeHandler.getDeviceInfo(deviceId);
logger.trace("Device initialized:\n{}", info);
var deviceInfo = bridgeHandler.getDeviceInfo(deviceId);
logger.trace("Device validated and initialized:\n{}", deviceInfo);
return deviceInfo;
} catch (TimeoutException | ExecutionException | BoschSHCException e) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
return;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
return;
}
super.initialize();
return null;
}
/**

View File

@ -49,6 +49,7 @@ import com.google.gson.JsonElement;
* @author Stefan Kästle - Initial contribution
* @author Christian Oeing - refactorings of e.g. server registration
* @author David Pace - Handler abstraction
* @author David Pace - Support for child device updates
*/
@NonNullByDefault
public abstract class BoschSHCHandler extends BaseThingHandler {
@ -154,7 +155,7 @@ public abstract class BoschSHCHandler extends BaseThingHandler {
* @param stateData Current state of device service. Serialized as JSON.
*/
public void processUpdate(String serviceName, @Nullable JsonElement stateData) {
// Check services of device to correctly
// Find service(s) with the specified name and propagate new state to them
for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
BoschSHCService<? extends BoschSHCServiceState> service = deviceService.service;
if (serviceName.equals(service.getServiceName())) {
@ -163,11 +164,23 @@ public abstract class BoschSHCHandler extends BaseThingHandler {
}
}
/**
* Processes an update for a logical child device.
*
* @param childDeviceId the ID of the logical child device
* @param serviceName the name of the service this update is targeted at
* @param stateData the new service state serialized as JSON
*/
public void processChildUpdate(String childDeviceId, String serviceName, @Nullable JsonElement stateData) {
// default implementation is empty, subclasses may override
}
/**
* Use this method to register all services of the device with
* {@link #registerService(BoschSHCService, Consumer, Collection, boolean)}.
*/
protected void initializeServices() throws BoschSHCException {
// default implementation is empty, subclasses may override
}
/**

View File

@ -17,9 +17,11 @@ import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConst
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_CLIMATE_CONTROL;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_INTRUSION_DETECTION_SYSTEM;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_LIGHT_CONTROL_2;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_MOTION_DETECTOR;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SHC;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL_2;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SMART_BULB;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SMART_PLUG_COMPACT;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SMOKE_DETECTOR;
@ -43,9 +45,11 @@ import org.openhab.binding.boschshc.internal.devices.bridge.BridgeHandler;
import org.openhab.binding.boschshc.internal.devices.camera.CameraHandler;
import org.openhab.binding.boschshc.internal.devices.climatecontrol.ClimateControlHandler;
import org.openhab.binding.boschshc.internal.devices.intrusion.IntrusionDetectionHandler;
import org.openhab.binding.boschshc.internal.devices.lightcontrol.LightControl2Handler;
import org.openhab.binding.boschshc.internal.devices.lightcontrol.LightControlHandler;
import org.openhab.binding.boschshc.internal.devices.motiondetector.MotionDetectorHandler;
import org.openhab.binding.boschshc.internal.devices.plug.PlugHandler;
import org.openhab.binding.boschshc.internal.devices.shuttercontrol.ShutterControl2Handler;
import org.openhab.binding.boschshc.internal.devices.shuttercontrol.ShutterControlHandler;
import org.openhab.binding.boschshc.internal.devices.smartbulb.SmartBulbHandler;
import org.openhab.binding.boschshc.internal.devices.smokedetector.SmokeDetector2Handler;
@ -109,6 +113,7 @@ public class BoschSHCHandlerFactory extends BaseThingHandlerFactory {
new ThingTypeHandlerMapping(THING_TYPE_WINDOW_CONTACT_2, WindowContact2Handler::new),
new ThingTypeHandlerMapping(THING_TYPE_MOTION_DETECTOR, MotionDetectorHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_SHUTTER_CONTROL, ShutterControlHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_SHUTTER_CONTROL_2, ShutterControl2Handler::new),
new ThingTypeHandlerMapping(THING_TYPE_THERMOSTAT, ThermostatHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_CLIMATE_CONTROL, ClimateControlHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_WALL_THERMOSTAT, WallThermostatHandler::new),
@ -123,7 +128,8 @@ public class BoschSHCHandlerFactory extends BaseThingHandlerFactory {
thing -> new UniversalSwitchHandler(thing, timeZoneProvider)),
new ThingTypeHandlerMapping(THING_TYPE_UNIVERSAL_SWITCH_2,
thing -> new UniversalSwitch2Handler(thing, timeZoneProvider)),
new ThingTypeHandlerMapping(THING_TYPE_SMOKE_DETECTOR_2, SmokeDetector2Handler::new));
new ThingTypeHandlerMapping(THING_TYPE_SMOKE_DETECTOR_2, SmokeDetector2Handler::new),
new ThingTypeHandlerMapping(THING_TYPE_LIGHT_CONTROL_2, LightControl2Handler::new));
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {

View File

@ -111,7 +111,32 @@ public class BoschHttpClient extends HttpClient {
* @return Bosch SHC URL for passed endpoint
*/
public String getBoschShcUrl(String endpoint) {
return String.format("https://%s:8444/%s", this.ipAddress, endpoint);
String url = String.format("https://%s:8444/%s", this.ipAddress, endpoint);
return escapeURL(url);
}
/**
* Performs specific URL escaping required for certain Bosch SHC URLs.
* <p>
* In particular, hash characters in child device IDs must be escaped with <code>%23</code>.
* <p>
* Invalid example:
*
* <pre>
* https://host:port/devices/hdm:ZigBee:70ac08fffe5294ea#3/services/PowerSwitch/state
* </pre>
*
* Valid example:
*
* <pre>
* https://host:port/devices/hdm:ZigBee:70ac08fffe5294ea%233/services/PowerSwitch/state
* </pre>
*
* @param url the URL to be escaped
* @return the escaped URL
*/
private String escapeURL(String url) {
return url.replace("#", "%23");
}
/**

View File

@ -34,6 +34,7 @@ import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.boschshc.internal.devices.BoschDeviceIdUtils;
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
@ -476,7 +477,7 @@ public class BridgeHandler extends BaseBridgeHandler {
*
* @param result Results from Long Polling
*/
private void handleLongPollResult(LongPollResult result) {
void handleLongPollResult(LongPollResult result) {
for (BoschSHCServiceState serviceState : result.result) {
if (serviceState instanceof DeviceServiceData deviceServiceData) {
handleDeviceServiceData(deviceServiceData);
@ -562,12 +563,7 @@ public class BridgeHandler extends BaseBridgeHandler {
*/
private void forwardStateToHandlers(BoschSHCServiceState serviceData, JsonElement state, String updateDeviceId) {
boolean handled = false;
final String serviceId;
if (serviceData instanceof UserDefinedState userState) {
serviceId = userState.getId();
} else {
serviceId = ((DeviceServiceData) serviceData).id;
}
final String serviceId = getServiceId(serviceData);
Bridge bridge = this.getThing();
for (Thing childThing : bridge.getThings()) {
@ -578,13 +574,17 @@ public class BridgeHandler extends BaseBridgeHandler {
@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, serviceId, state);
handler.processUpdate(serviceId, state);
if (deviceId == null) {
continue;
}
logger.trace("Checking device {}, looking for {}", deviceId, updateDeviceId);
// handled is a boolean latch that stays true once it becomes true
// note that no short-circuiting operators are used, meaning that the method
// calls will always be evaluated, even if the latch is already true
handled |= notifyHandler(handler, deviceId, updateDeviceId, serviceId, state);
handled |= notifyParentHandler(handler, deviceId, updateDeviceId, serviceId, state);
} else {
logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler);
}
@ -595,6 +595,61 @@ public class BridgeHandler extends BaseBridgeHandler {
}
}
/**
* Notifies the given handler if its device ID exactly matches the device ID for which the update was received.
*
* @param handler the handler to be notified if applicable
* @param deviceId the device ID associated with the handler
* @param updateDeviceId the device ID for which the update was received
* @param serviceId the ID of the service for which the update was received
* @param state the received state object as JSON element
*
* @return <code>true</code> if the handler matched and was notified, <code>false</code> otherwise
*/
private boolean notifyHandler(BoschSHCHandler handler, String deviceId, String updateDeviceId, String serviceId,
JsonElement state) {
if (updateDeviceId.equals(deviceId)) {
logger.debug("Found handler {}, calling processUpdate() for service {} with state {}", handler, serviceId,
state);
handler.processUpdate(serviceId, state);
return true;
}
return false;
}
/**
* If an update is received for a logical child device and the given handler is the parent device handler, the
* parent handler is notified.
*
* @param handler the handler to be notified if applicable
* @param deviceId the device ID associated with the handler
* @param updateDeviceId the device ID for which the update was received
* @param serviceId the ID of the service for which the update was received
* @param state the received state object as JSON element
* @return <code>true</code> if the given handler was the corresponding parent handler and was notified,
* <code>false</code> otherwise
*/
private boolean notifyParentHandler(BoschSHCHandler handler, String deviceId, String updateDeviceId,
String serviceId, JsonElement state) {
if (BoschDeviceIdUtils.isChildDeviceId(updateDeviceId)) {
String parentDeviceId = BoschDeviceIdUtils.getParentDeviceId(updateDeviceId);
if (parentDeviceId.equals(deviceId)) {
logger.debug("Notifying parent handler {} about update for child device for service {} with state {}",
handler, serviceId, state);
handler.processChildUpdate(updateDeviceId, serviceId, state);
return true;
}
}
return false;
}
private String getServiceId(BoschSHCServiceState serviceData) {
if (serviceData instanceof UserDefinedState userState) {
return userState.getId();
}
return ((DeviceServiceData) serviceData).id;
}
/**
* Bridge callback handler for the failures during long polls.
*

View File

@ -18,23 +18,25 @@ import com.google.gson.annotations.SerializedName;
/**
* Represents a single devices connected to the Bosch Smart Home Controller.
*
* Example from Json:
*
* <p>
* Example JSON:
*
* <pre>
* {
* "@type":"device",
* "rootDeviceId":"64-da-a0-02-14-9b",
* "id":"hdm:HomeMaticIP:3014F711A00004953859F31B",
* "deviceServiceIds":["PowerMeter","PowerSwitch","PowerSwitchProgram","Routing"],
* "manufacturer":"BOSCH",
* "roomId":"hz_3",
* "deviceModel":"PSM",
* "serial":"3014F711A00004953859F31B",
* "profile":"GENERIC",
* "name":"Coffee Machine",
* "status":"AVAILABLE",
* "childDeviceIds":[]
* "@type": "device",
* "rootDeviceId": "64-da-a0-02-14-9b",
* "id": "hdm:HomeMaticIP:3014F711A00004953859F31B",
* "deviceServiceIds": ["PowerMeter","PowerSwitch","PowerSwitchProgram","Routing"],
* "manufacturer": "BOSCH",
* "roomId": "hz_3",
* "deviceModel": "PSM",
* "serial": "3014F711A00004953859F31B",
* "profile": "GENERIC",
* "name": "Coffee Machine",
* "status": "AVAILABLE",
* "childDeviceIds": []
* }
* </pre>
*
* @author Stefan Kästle - Initial contribution
*/

View File

@ -18,25 +18,30 @@ import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
/**
* Response of the Controller for a Long Poll API call.
* <p>
* Example JSON:
*
* <pre>
* {
* "result": [{
* "path": "/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch",
* "@type": "DeviceServiceData",
* "id": "PowerSwitch",
* "state": {
* "@type": "powerSwitchState",
* "switchState": "ON"
* },
* "deviceId": "hdm:HomeMaticIP:3014F711A0001916D859A8A9"
* }],
* "jsonrpc": "2.0"
* }
* </pre>
*
* @author Stefan Kästle - Initial contribution
*/
public class LongPollResult {
/**
* {"result":[
* ..{
* ...."path":"/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch",
* ...."@type":"DeviceServiceData",
* ...."id":"PowerSwitch",
* ...."state":{
* ......"@type":"powerSwitchState",
* ......"switchState":"ON"
* ....},
* ...."deviceId":"hdm:HomeMaticIP:3014F711A0001916D859A8A9"}
* ],"jsonrpc":"2.0"}
*/
public ArrayList<BoschSHCServiceState> result;
public String jsonrpc;
}

View File

@ -0,0 +1,240 @@
/**
* Copyright (c) 2010-2024 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.lightcontrol;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION_1;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION_2;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_POWER_SWITCH_1;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_POWER_SWITCH_2;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_SIGNAL_STRENGTH;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.boschshc.internal.devices.BoschSHCDeviceHandler;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.childprotection.ChildProtectionService;
import org.openhab.binding.boschshc.internal.services.childprotection.dto.ChildProtectionServiceState;
import org.openhab.binding.boschshc.internal.services.communicationquality.CommunicationQualityService;
import org.openhab.binding.boschshc.internal.services.communicationquality.dto.CommunicationQualityServiceState;
import org.openhab.binding.boschshc.internal.services.powermeter.PowerMeterService;
import org.openhab.binding.boschshc.internal.services.powermeter.dto.PowerMeterServiceState;
import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchService;
import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState;
import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonElement;
/**
* Handler for Light Control II devices.
* <p>
* This implementation handles both common channels and specific channels of the
* two logical child devices.
*
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
public class LightControl2Handler extends BoschSHCDeviceHandler {
private final Logger logger = LoggerFactory.getLogger(LightControl2Handler.class);
private @Nullable String childDeviceId1;
private @Nullable String childDeviceId2;
private PowerSwitchService lightSwitchCircuit1PowerSwitchService;
private PowerSwitchService lightSwitchCircuit2PowerSwitchService;
private ChildProtectionService lightSwitchCircuit1ChildProtectionService;
private ChildProtectionService lightSwitchCircuit2ChildProtectionService;
public LightControl2Handler(Thing thing) {
super(thing);
lightSwitchCircuit1PowerSwitchService = new PowerSwitchService();
lightSwitchCircuit2PowerSwitchService = new PowerSwitchService();
lightSwitchCircuit1ChildProtectionService = new ChildProtectionService();
lightSwitchCircuit2ChildProtectionService = new ChildProtectionService();
}
@Override
protected boolean processDeviceInfo(Device deviceInfo) {
super.processDeviceInfo(deviceInfo);
logger.debug("Initializing child devices of Light Control II, child device IDs from device info: {}",
deviceInfo.childDeviceIds);
if (deviceInfo.childDeviceIds == null || deviceInfo.childDeviceIds.size() != 2) {
updateStatusChildDeviceIDsNotObtainable();
return false;
}
List<String> childDeviceIds = new ArrayList<>(deviceInfo.childDeviceIds);
// since we were not sure whether the child device ID order is always the same,
// we ensure a deterministic order by sorting the child IDs
// see https://github.com/openhab/openhab-addons/pull/16400#discussion_r1497762612
Collections.sort(childDeviceIds);
logger.trace("Child device IDs for Light Control II after sorting: {}", childDeviceIds);
if (validateDeviceId(childDeviceIds.get(0)) == null || validateDeviceId(childDeviceIds.get(1)) == null) {
updateStatusChildDeviceIDsNotObtainable();
return false;
}
childDeviceId1 = childDeviceIds.get(0);
childDeviceId2 = childDeviceIds.get(1);
logger.debug("Child device IDs for Light Control II configured successfully.");
return true;
}
private void updateStatusChildDeviceIDsNotObtainable() {
super.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.conf-error.child-device-ids-not-obtainable");
}
@Override
protected void initializeServices() throws BoschSHCException {
super.initializeServices();
createService(CommunicationQualityService::new, this::updateChannels, List.of(CHANNEL_SIGNAL_STRENGTH), true);
createService(PowerMeterService::new, this::updateChannels,
List.of(CHANNEL_POWER_CONSUMPTION, CHANNEL_ENERGY_CONSUMPTION), true);
// local variable required to ensure non-nullness, member can theoretically be modified
String lChildDeviceId1 = childDeviceId1;
if (lChildDeviceId1 == null) {
throw new BoschSHCException("Child device ID 1 is not set for thing " + getThing().getUID());
}
// local variable required to ensure non-nullness, member can theoretically be modified
String lChildDeviceId2 = childDeviceId2;
if (lChildDeviceId2 == null) {
throw new BoschSHCException("Child device ID 2 is not set for thing " + getThing().getUID());
}
lightSwitchCircuit1PowerSwitchService.initialize(getBridgeHandler(), lChildDeviceId1,
state -> updatePowerSwitchChannel(state, CHANNEL_POWER_SWITCH_1));
lightSwitchCircuit2PowerSwitchService.initialize(getBridgeHandler(), lChildDeviceId2,
state -> updatePowerSwitchChannel(state, CHANNEL_POWER_SWITCH_2));
lightSwitchCircuit1ChildProtectionService.initialize(getBridgeHandler(), lChildDeviceId1,
state -> updateChildProtectionChannel(state, CHANNEL_CHILD_PROTECTION_1));
lightSwitchCircuit2ChildProtectionService.initialize(getBridgeHandler(), lChildDeviceId2,
state -> updateChildProtectionChannel(state, CHANNEL_CHILD_PROTECTION_2));
}
private void updateChannels(CommunicationQualityServiceState communicationQualityServiceState) {
updateState(CHANNEL_SIGNAL_STRENGTH, communicationQualityServiceState.quality.toSystemSignalStrength());
}
/**
* Updates the channels which are linked to the {@link PowerMeterService} of the
* device.
*
* @param state Current state of {@link PowerMeterService}.
*/
private void updateChannels(PowerMeterServiceState state) {
super.updateState(CHANNEL_POWER_CONSUMPTION, new QuantityType<>(state.powerConsumption, Units.WATT));
super.updateState(CHANNEL_ENERGY_CONSUMPTION, new QuantityType<>(state.energyConsumption, Units.WATT_HOUR));
}
@Override
public void processChildUpdate(String childDeviceId, String serviceName, @Nullable JsonElement stateData) {
super.processChildUpdate(childDeviceId, serviceName, stateData);
if (PowerSwitchService.POWER_SWITCH_SERVICE_NAME.equals(serviceName)) {
if (childDeviceId.equals(childDeviceId1)) {
lightSwitchCircuit1PowerSwitchService.onStateUpdate(stateData);
} else if (childDeviceId.equals(childDeviceId2)) {
lightSwitchCircuit2PowerSwitchService.onStateUpdate(stateData);
}
} else if (ChildProtectionService.CHILD_PROTECTION_SERVICE_NAME.equals(serviceName)) {
if (childDeviceId.equals(childDeviceId1)) {
lightSwitchCircuit1ChildProtectionService.onStateUpdate(stateData);
} else if (childDeviceId.equals(childDeviceId2)) {
lightSwitchCircuit2ChildProtectionService.onStateUpdate(stateData);
}
}
}
/**
* Updates the power switch channel for one of the child devices.
*
* @param state the new {@link PowerSwitchServiceState}
* @param channelId the power switch channel ID associated with the child device
*/
private void updatePowerSwitchChannel(PowerSwitchServiceState state, String channelId) {
State powerState = OnOffType.from(state.switchState.toString());
super.updateState(channelId, powerState);
}
/**
* Updates the child protection channel for one of the child devices.
*
* @param state the new {@link ChildProtectionServiceState}
* @param channelId the child protection channel ID associated with the child
* device
*/
private void updateChildProtectionChannel(ChildProtectionServiceState state, String channelId) {
super.updateState(channelId, OnOffType.from(state.childLockActive));
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
if (CHANNEL_POWER_SWITCH_1.equals(channelUID.getId()) && (command instanceof OnOffType onOffCommand)) {
updatePowerSwitchState(onOffCommand, lightSwitchCircuit1PowerSwitchService);
} else if (CHANNEL_POWER_SWITCH_2.equals(channelUID.getId()) && (command instanceof OnOffType onOffCommand)) {
updatePowerSwitchState(onOffCommand, lightSwitchCircuit2PowerSwitchService);
} else if (CHANNEL_CHILD_PROTECTION_1.equals(channelUID.getId())
&& (command instanceof OnOffType onOffCommand)) {
updateChildProtectionState(onOffCommand, lightSwitchCircuit1ChildProtectionService);
} else if (CHANNEL_CHILD_PROTECTION_2.equals(channelUID.getId())
&& (command instanceof OnOffType onOffCommand)) {
updateChildProtectionState(onOffCommand, lightSwitchCircuit2ChildProtectionService);
}
}
private void updatePowerSwitchState(OnOffType command, PowerSwitchService powerSwitchService) {
PowerSwitchServiceState state = new PowerSwitchServiceState();
state.switchState = PowerSwitchState.valueOf(command.toFullString());
this.updateServiceState(powerSwitchService, state);
}
private void updateChildProtectionState(OnOffType onOffCommand, ChildProtectionService childProtectionService) {
ChildProtectionServiceState childProtectionServiceState = new ChildProtectionServiceState();
childProtectionServiceState.childLockActive = onOffCommand == OnOffType.ON;
updateServiceState(childProtectionService, childProtectionServiceState);
}
}

View File

@ -13,7 +13,7 @@
package org.openhab.binding.boschshc.internal.devices.lightcontrol;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandler;
import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerWithPowerMeter;
import org.openhab.core.thing.Thing;
/**
@ -22,7 +22,7 @@ import org.openhab.core.thing.Thing;
* @author Stefan Kästle - Initial contribution
*/
@NonNullByDefault
public class LightControlHandler extends AbstractPowerSwitchHandler {
public class LightControlHandler extends AbstractPowerSwitchHandlerWithPowerMeter {
public LightControlHandler(Thing thing) {
super(thing);

View File

@ -13,7 +13,7 @@
package org.openhab.binding.boschshc.internal.devices.plug;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandler;
import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerWithPowerMeter;
import org.openhab.core.thing.Thing;
/**
@ -22,7 +22,7 @@ import org.openhab.core.thing.Thing;
* @author David Pace - Initial contribution
*/
@NonNullByDefault
public class PlugHandler extends AbstractPowerSwitchHandler {
public class PlugHandler extends AbstractPowerSwitchHandlerWithPowerMeter {
public PlugHandler(Thing thing) {
super(thing);

View File

@ -0,0 +1,94 @@
/**
* Copyright (c) 2010-2024 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.shuttercontrol;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_SIGNAL_STRENGTH;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.childprotection.ChildProtectionService;
import org.openhab.binding.boschshc.internal.services.childprotection.dto.ChildProtectionServiceState;
import org.openhab.binding.boschshc.internal.services.communicationquality.CommunicationQualityService;
import org.openhab.binding.boschshc.internal.services.communicationquality.dto.CommunicationQualityServiceState;
import org.openhab.binding.boschshc.internal.services.powermeter.PowerMeterService;
import org.openhab.binding.boschshc.internal.services.powermeter.dto.PowerMeterServiceState;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
/**
* Handler for second generation shutter controls.
* <p>
* This handler is used if Shutter/Light Control II devices are configured as shutter controls.
*
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
public class ShutterControl2Handler extends ShutterControlHandler {
private final ChildProtectionService childProtectionService;
public ShutterControl2Handler(Thing thing) {
super(thing);
this.childProtectionService = new ChildProtectionService();
}
@Override
protected void initializeServices() throws BoschSHCException {
super.initializeServices();
createService(CommunicationQualityService::new, this::updateChannels, List.of(CHANNEL_SIGNAL_STRENGTH), true);
registerService(childProtectionService, this::updateChannels, List.of(CHANNEL_CHILD_PROTECTION), true);
createService(PowerMeterService::new, this::updateChannels,
List.of(CHANNEL_POWER_CONSUMPTION, CHANNEL_ENERGY_CONSUMPTION), true);
}
private void updateChannels(CommunicationQualityServiceState communicationQualityServiceState) {
updateState(CHANNEL_SIGNAL_STRENGTH, communicationQualityServiceState.quality.toSystemSignalStrength());
}
private void updateChannels(ChildProtectionServiceState childProtectionServiceState) {
updateState(CHANNEL_CHILD_PROTECTION, OnOffType.from(childProtectionServiceState.childLockActive));
}
private void updateChannels(PowerMeterServiceState state) {
super.updateState(CHANNEL_POWER_CONSUMPTION, new QuantityType<>(state.powerConsumption, Units.WATT));
super.updateState(CHANNEL_ENERGY_CONSUMPTION, new QuantityType<>(state.energyConsumption, Units.WATT_HOUR));
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
if (BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION.equals(channelUID.getId())
&& (command instanceof OnOffType onOffCommand)) {
updateChildProtectionState(onOffCommand);
}
}
private void updateChildProtectionState(OnOffType onOffCommand) {
ChildProtectionServiceState childProtectionServiceState = new ChildProtectionServiceState();
childProtectionServiceState.childLockActive = onOffCommand == OnOffType.ON;
updateServiceState(childProtectionService, childProtectionServiceState);
}
}

View File

@ -56,6 +56,11 @@ public class ThingDiscoveryService extends AbstractThingHandlerDiscoveryService<
private final Logger logger = LoggerFactory.getLogger(ThingDiscoveryService.class);
/**
* Device model representing logical child devices of Light Control II
*/
static final String DEVICE_MODEL_LIGHT_CONTROL_CHILD_DEVICE = "MICROMODULE_LIGHT_ATTACHED";
protected static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(
BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH, BoschSHCBindingConstants.THING_TYPE_TWINGUARD,
BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT, BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT_2,
@ -89,7 +94,10 @@ public class ThingDiscoveryService extends AbstractThingHandlerDiscoveryService<
new AbstractMap.SimpleEntry<>("TRV", BoschSHCBindingConstants.THING_TYPE_THERMOSTAT),
new AbstractMap.SimpleEntry<>("WRC2", BoschSHCBindingConstants.THING_TYPE_UNIVERSAL_SWITCH),
new AbstractMap.SimpleEntry<>("SWITCH2", BoschSHCBindingConstants.THING_TYPE_UNIVERSAL_SWITCH_2),
new AbstractMap.SimpleEntry<>("SMOKE_DETECTOR2", BoschSHCBindingConstants.THING_TYPE_SMOKE_DETECTOR_2)
new AbstractMap.SimpleEntry<>("SMOKE_DETECTOR2", BoschSHCBindingConstants.THING_TYPE_SMOKE_DETECTOR_2),
new AbstractMap.SimpleEntry<>("MICROMODULE_SHUTTER", BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL_2),
new AbstractMap.SimpleEntry<>("MICROMODULE_AWNING", BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL_2),
new AbstractMap.SimpleEntry<>("MICROMODULE_LIGHT_CONTROL", BoschSHCBindingConstants.THING_TYPE_LIGHT_CONTROL_2)
// Future Extension: map deviceModel names to BoschSHC Thing Types when they are supported
// new AbstractMap.SimpleEntry<>("SMOKE_DETECTION_SYSTEM", BoschSHCBindingConstants.),
// new AbstractMap.SimpleEntry<>("PRESENCE_SIMULATION_SERVICE", BoschSHCBindingConstants.),
@ -219,13 +227,15 @@ public class ThingDiscoveryService extends AbstractThingHandlerDiscoveryService<
logger.trace("- got thingTypeID '{}' for deviceModel '{}'", thingTypeUID.getId(), device.deviceModel);
ThingUID thingUID = new ThingUID(thingTypeUID, thingHandler.getThing().getUID(), device.id.replace(':', '_'));
ThingUID thingUID = new ThingUID(thingTypeUID, thingHandler.getThing().getUID(),
buildCompliantThingID(device.id));
logger.trace("- got thingUID '{}' for device: '{}'", thingUID, device);
DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID)
.withProperty("id", device.id).withLabel(getNiceName(device.name, roomName));
discoveryResult.withBridge(thingHandler.getThing().getUID());
if (!roomName.isEmpty()) {
discoveryResult.withProperty("Location", roomName);
}
@ -235,6 +245,18 @@ public class ThingDiscoveryService extends AbstractThingHandlerDiscoveryService<
thingUID, thingTypeUID, device.id, device.deviceModel);
}
/**
* Translates a Bosch device ID to an openHAB-compliant thing ID.
* <p>
* Characters that are not allowed in thing IDs are replaced by underscores.
*
* @param deviceId the Bosch device ID
* @return the translated openHAB-compliant thing ID
*/
private String buildCompliantThingID(String deviceId) {
return deviceId.replace(':', '_').replace('#', '_');
}
private String getNiceName(String name, String roomName) {
if (!name.startsWith("-")) {
return name;
@ -268,6 +290,15 @@ public class ThingDiscoveryService extends AbstractThingHandlerDiscoveryService<
if (thingTypeId != null) {
return new ThingTypeUID(BoschSHCBindingConstants.BINDING_ID, thingTypeId.getId());
}
if (DEVICE_MODEL_LIGHT_CONTROL_CHILD_DEVICE.equals(device.deviceModel)) {
// Light Control II exposes a parent device and two child devices.
// We only add one thing for the parent device and the child devices are logically included.
// Therefore we do not need to add separate things for the child devices and need to suppress the
// log entry about the unknown device model.
return null;
}
logger.debug("Unknown deviceModel '{}'! Please create a support request issue for this unknown device model.",
device.deviceModel);
return null;

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2024 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.childprotection;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.services.BoschSHCService;
import org.openhab.binding.boschshc.internal.services.childprotection.dto.ChildProtectionServiceState;
/**
* Service to activate and deactivate child protection.
*
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
public class ChildProtectionService extends BoschSHCService<ChildProtectionServiceState> {
public static final String CHILD_PROTECTION_SERVICE_NAME = "ChildProtection";
public ChildProtectionService() {
super(CHILD_PROTECTION_SERVICE_NAME, ChildProtectionServiceState.class);
}
}

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2024 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.childprotection.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
/**
* State of the child protection service.
*
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
public class ChildProtectionServiceState extends BoschSHCServiceState {
public ChildProtectionServiceState() {
super("ChildProtectionState");
}
public boolean childLockActive;
}

View File

@ -24,7 +24,9 @@ import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitc
@NonNullByDefault
public class PowerSwitchService extends BoschSHCService<PowerSwitchServiceState> {
public static final String POWER_SWITCH_SERVICE_NAME = "PowerSwitch";
public PowerSwitchService() {
super("PowerSwitch", PowerSwitchServiceState.class);
super(POWER_SWITCH_SERVICE_NAME, PowerSwitchServiceState.class);
}
}

View File

@ -3,6 +3,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:boschshc:bridge">
<parameter name="ipAddress" type="text" required="true">
<context>network-address</context>
@ -15,16 +16,19 @@
<description>The system password of the Bosch Smart Home Controller necessary for pairing.</description>
</parameter>
</config-description>
<config-description uri="thing-type:boschshc:device">
<parameter name="id" type="text" required="true">
<label>Device ID</label>
<description>Unique ID of the device.</description>
</parameter>
</config-description>
<config-description uri="thing-type:boschshc:user-defined-state">
<parameter name="id" type="text" required="true">
<label>State ID</label>
<description>Unique ID of the state.</description>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -11,6 +11,8 @@ thing-type.boschshc.in-wall-switch.label = In-wall Switch
thing-type.boschshc.in-wall-switch.description = A simple light control.
thing-type.boschshc.intrusion-detection-system.label = Intrusion Detection System
thing-type.boschshc.intrusion-detection-system.description = Allows to retrieve and control the state of the intrusion detection alarm system.
thing-type.boschshc.light-control-2.label = Light Control II
thing-type.boschshc.light-control-2.description = Advanced light control with two switch circuits.
thing-type.boschshc.motion-detector.label = Motion Detector
thing-type.boschshc.motion-detector.description = Detects every movement through an intelligent combination of passive infra-red technology and an additional temperature sensor.
thing-type.boschshc.security-camera-360.label = Security Camera 360
@ -19,6 +21,8 @@ thing-type.boschshc.security-camera-eyes.label = Security Camera Eyes
thing-type.boschshc.security-camera-eyes.description = Outdoor security camera with motion detection and light.
thing-type.boschshc.shc.label = Smart Home Controller
thing-type.boschshc.shc.description = The Bosch Smart Home Bridge representing the Bosch Smart Home Controller.
thing-type.boschshc.shutter-control-2.label = Shutter Control II
thing-type.boschshc.shutter-control-2.description = Second generation shutter control.
thing-type.boschshc.shutter-control.label = Shutter Control
thing-type.boschshc.shutter-control.description = Control of your shutter to take any position you desire.
thing-type.boschshc.smart-bulb.label = Smart Bulb
@ -87,6 +91,8 @@ channel-type.boschshc.camera-notification.state.option.ON = Enabled
channel-type.boschshc.camera-notification.state.option.OFF = Disabled
channel-type.boschshc.child-lock.label = Child Lock
channel-type.boschshc.child-lock.description = Enables or disables the child lock on the device.
channel-type.boschshc.child-protection.label = Child Protection
channel-type.boschshc.child-protection.description = Enables or disables the child protection on the device.
channel-type.boschshc.combined-rating.label = Combined Rating
channel-type.boschshc.combined-rating.description = Combined rating of the air quality.
channel-type.boschshc.combined-rating.state.option.GOOD = Good Quality
@ -185,3 +191,4 @@ offline.conf-error.empty-device-id = No device ID set.
offline.conf-error.invalid-device-id = Device ID is invalid.
offline.conf-error.empty-state-id = No ID set.
offline.conf-error.invalid-state-id = ID is invalid.
offline.conf-error.child-device-ids-not-obtainable = Could not obtain child device IDs.

View File

@ -163,6 +163,48 @@
</thing-type>
<thing-type id="shutter-control-2">
<supported-bridge-type-refs>
<bridge-type-ref id="shc"/>
</supported-bridge-type-refs>
<label>Shutter Control II</label>
<description>Second generation shutter control.</description>
<channels>
<channel id="level" typeId="level"/>
<channel id="signal-strength" typeId="system.signal-strength"/>
<channel id="child-protection" typeId="child-protection"/>
<channel id="power-consumption" typeId="power-consumption"/>
<channel id="energy-consumption" typeId="energy-consumption"/>
</channels>
<config-description-ref uri="thing-type:boschshc:device"/>
</thing-type>
<thing-type id="light-control-2">
<supported-bridge-type-refs>
<bridge-type-ref id="shc"/>
</supported-bridge-type-refs>
<label>Light Control II</label>
<description>Advanced light control with two switch circuits.</description>
<channels>
<channel id="signal-strength" typeId="system.signal-strength"/>
<channel id="power-consumption" typeId="power-consumption"/>
<channel id="energy-consumption" typeId="energy-consumption"/>
<channel id="power-switch-1" typeId="system.power"/>
<channel id="child-protection-1" typeId="child-protection"/>
<channel id="power-switch-2" typeId="system.power"/>
<channel id="child-protection-2" typeId="child-protection"/>
</channels>
<config-description-ref uri="thing-type:boschshc:device"/>
</thing-type>
<thing-type id="thermostat">
<supported-bridge-type-refs>
<bridge-type-ref id="shc"/>
@ -703,4 +745,10 @@
<state readOnly="true"/>
</channel-type>
<channel-type id="child-protection">
<item-type>Switch</item-type>
<label>Child Protection</label>
<description>Enables or disables the child protection on the device.</description>
</channel-type>
</thing:thing-descriptions>

View File

@ -13,6 +13,7 @@
package org.openhab.binding.boschshc.internal.devices;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
import org.openhab.core.config.core.Configuration;
/**
@ -26,6 +27,13 @@ import org.openhab.core.config.core.Configuration;
public abstract class AbstractBoschSHCDeviceHandlerTest<T extends BoschSHCDeviceHandler>
extends AbstractBoschSHCHandlerTest<T> {
@Override
protected void configureDevice(Device device) {
super.configureDevice(device);
device.id = getDeviceID();
}
@Override
protected Configuration getConfiguration() {
Configuration configuration = super.getConfiguration();

View File

@ -12,8 +12,13 @@
*/
package org.openhab.binding.boschshc.internal.devices;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
@ -25,6 +30,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.binding.boschshc.internal.devices.bridge.BridgeHandler;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Bridge;
@ -58,6 +64,8 @@ public abstract class AbstractBoschSHCHandlerTest<T extends BoschSHCHandler> {
private @Mock @NonNullByDefault({}) ThingHandlerCallback callback;
private @NonNullByDefault({}) Device device;
protected AbstractBoschSHCHandlerTest() {
this.fixture = createFixture();
}
@ -72,6 +80,10 @@ public abstract class AbstractBoschSHCHandlerTest<T extends BoschSHCHandler> {
when(bridge.getHandler()).thenReturn(bridgeHandler);
lenient().when(thing.getConfiguration()).thenReturn(getConfiguration());
device = new Device();
configureDevice(device);
lenient().when(bridgeHandler.getDeviceInfo(anyString())).thenReturn(device);
fixture.initialize();
}
@ -107,6 +119,14 @@ public abstract class AbstractBoschSHCHandlerTest<T extends BoschSHCHandler> {
return callback;
}
protected Device getDevice() {
return device;
}
protected void configureDevice(Device device) {
// abstract implementation is empty, subclasses may override
}
@Test
public void testInitialize() {
ThingStatusInfo expectedStatusInfo = new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);

View File

@ -12,28 +12,26 @@
*/
package org.openhab.binding.boschshc.internal.devices;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import javax.measure.quantity.Energy;
import javax.measure.quantity.Power;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.powermeter.dto.PowerMeterServiceState;
import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState;
import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.types.RefreshType;
import com.google.gson.JsonElement;
@ -52,10 +50,6 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
private @Captor @NonNullByDefault({}) ArgumentCaptor<PowerSwitchServiceState> serviceStateCaptor;
private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Power>> powerCaptor;
private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Energy>> energyCaptor;
@BeforeEach
@Override
public void beforeEach() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
@ -65,12 +59,6 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
powerSwitchServiceState.switchState = PowerSwitchState.ON;
lenient().when(bridgeHandler.getState(anyString(), eq("PowerSwitch"), same(PowerSwitchServiceState.class)))
.thenReturn(powerSwitchServiceState);
PowerMeterServiceState powerMeterServiceState = new PowerMeterServiceState();
powerMeterServiceState.powerConsumption = 12.34d;
powerMeterServiceState.energyConsumption = 56.78d;
lenient().when(bridgeHandler.getState(anyString(), eq("PowerMeter"), same(PowerMeterServiceState.class)))
.thenReturn(powerMeterServiceState);
}
@Test
@ -101,47 +89,9 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.OFF);
}
@Test
public void testUpdateChannelPowerMeterServiceState() {
JsonElement jsonObject = JsonParser.parseString("""
{
"@type": "powerMeterState",
"powerConsumption": "23",
"energyConsumption": 42
}\
""");
getFixture().processUpdate("PowerMeter", jsonObject);
verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION)),
powerCaptor.capture());
QuantityType<Power> powerValue = powerCaptor.getValue();
assertEquals(23, powerValue.intValue());
verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION)),
energyCaptor.capture());
QuantityType<Energy> energyValue = energyCaptor.getValue();
assertEquals(42, energyValue.intValue());
}
@Test
public void testHandleCommandRefreshPowerSwitchChannel() {
getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), RefreshType.REFRESH);
verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.ON);
}
@Test
public void testHandleCommandRefreshPowerConsumptionChannel() {
getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION),
RefreshType.REFRESH);
verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION),
new QuantityType<>(12.34d, Units.WATT));
}
@Test
public void testHandleCommandRefreshEnergyConsumptionChannel() {
getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION),
RefreshType.REFRESH);
verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION),
new QuantityType<>(56.78d, Units.WATT_HOUR));
}
}

View File

@ -0,0 +1,105 @@
/**
* Copyright (c) 2010-2024 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.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verify;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import javax.measure.quantity.Energy;
import javax.measure.quantity.Power;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.powermeter.dto.PowerMeterServiceState;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.types.RefreshType;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
/**
* Abstract unit test implementation for power switch handler with power meter support.
*
* @author David Pace - Initial contribution
*
* @param <T> type of the handler to be tested
*/
@NonNullByDefault
public abstract class AbstractPowerSwitchHandlerWithPowerMeterTest<T extends AbstractPowerSwitchHandlerWithPowerMeter>
extends AbstractPowerSwitchHandlerTest<T> {
private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Power>> powerCaptor;
private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Energy>> energyCaptor;
@BeforeEach
public void beforeEach() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
super.beforeEach();
PowerMeterServiceState powerMeterServiceState = new PowerMeterServiceState();
powerMeterServiceState.powerConsumption = 12.34d;
powerMeterServiceState.energyConsumption = 56.78d;
lenient().when(bridgeHandler.getState(anyString(), eq("PowerMeter"), same(PowerMeterServiceState.class)))
.thenReturn(powerMeterServiceState);
}
@Test
public void testUpdateChannelPowerMeterServiceState() {
JsonElement jsonObject = JsonParser.parseString("""
{
"@type": "powerMeterState",
"powerConsumption": "23",
"energyConsumption": 42
}\
""");
getFixture().processUpdate("PowerMeter", jsonObject);
verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION)),
powerCaptor.capture());
QuantityType<Power> powerValue = powerCaptor.getValue();
assertEquals(23, powerValue.intValue());
verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION)),
energyCaptor.capture());
QuantityType<Energy> energyValue = energyCaptor.getValue();
assertEquals(42, energyValue.intValue());
}
@Test
public void testHandleCommandRefreshPowerConsumptionChannel() {
getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION),
RefreshType.REFRESH);
verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION),
new QuantityType<>(12.34d, Units.WATT));
}
@Test
public void testHandleCommandRefreshEnergyConsumptionChannel() {
getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION),
RefreshType.REFRESH);
verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION),
new QuantityType<>(56.78d, Units.WATT_HOUR));
}
}

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2024 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.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
/**
* Unit tests for {@link BoschDeviceIdUtils}.
*
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
class BoschDeviceIdUtilsTest {
@Test
void testIsChildDeviceId() {
assertFalse(BoschDeviceIdUtils.isChildDeviceId("hdm:ZigBee:70ac08fffe5294ea"));
assertTrue(BoschDeviceIdUtils.isChildDeviceId("hdm:ZigBee:70ac08fffe5294ea#3"));
}
@Test
void testGetParentDeviceId() {
assertEquals("hdm:ZigBee:70ac08fffe5294ea",
BoschDeviceIdUtils.getParentDeviceId("hdm:ZigBee:70ac08fffe5294ea#3"));
assertEquals("hdm:ZigBee:70ac08fffe5294ea",
BoschDeviceIdUtils.getParentDeviceId("hdm:ZigBee:70ac08fffe5294ea"));
}
}

View File

@ -101,6 +101,13 @@ class BoschHttpClientTest {
httpClient.getServiceStateUrl("testService", "testDevice", UserStateServiceState.class));
}
@Test
void getServiceStateUrlForChildDevice() {
assertEquals(
"https://127.0.0.1:8444/smarthome/devices/hdm:ZigBee:70ac08fffe5294ea%233/services/PowerSwitch/state",
httpClient.getServiceStateUrl("PowerSwitch", "hdm:ZigBee:70ac08fffe5294ea#3"));
}
@Test
void isAccessPossible() throws InterruptedException {
assertFalse(httpClient.isAccessPossible());

View File

@ -12,14 +12,27 @@
*/
package org.openhab.binding.boschshc.internal.devices.bridge;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -38,10 +51,12 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
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.DeviceServiceData;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceTest;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Faults;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedStateTest;
@ -62,6 +77,9 @@ import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
/**
* Unit tests for the {@link BridgeHandler}.
*
@ -77,6 +95,11 @@ class BridgeHandlerTest {
private @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
/**
* A mocked bridge instance
*/
private @NonNullByDefault({}) Bridge thing;
@BeforeAll
static void beforeAll() throws IOException {
Path mavenTargetFolder = Paths.get("target");
@ -102,7 +125,7 @@ class BridgeHandlerTest {
properties.put("password", "test");
bridgeConfiguration.setProperties(properties);
Thing thing = mock(Bridge.class);
thing = mock(Bridge.class);
when(thing.getConfiguration()).thenReturn(bridgeConfiguration);
// this calls initialize() as well
fixture.thingUpdated(thing);
@ -502,4 +525,129 @@ class BridgeHandlerTest {
void afterEach() throws Exception {
fixture.dispose();
}
@Test
void handleLongPollResultNoDeviceId() {
List<Thing> things = new ArrayList<Thing>();
when(thing.getThings()).thenReturn(things);
Thing thing = mock(Thing.class);
things.add(thing);
BoschSHCHandler thingHandler = mock(BoschSHCHandler.class);
when(thing.getHandler()).thenReturn(thingHandler);
String json = """
{
"result": [{
"path": "/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch",
"@type": "DeviceServiceData",
"id": "PowerSwitch",
"state": {
"@type": "powerSwitchState",
"switchState": "ON"
},
"deviceId": "hdm:HomeMaticIP:3014F711A0001916D859A8A9"
}],
"jsonrpc": "2.0"
}
""";
LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, LongPollResult.class);
assertNotNull(longPollResult);
fixture.handleLongPollResult(longPollResult);
verify(thingHandler).getBoschID();
verifyNoMoreInteractions(thingHandler);
}
@Test
void handleLongPollResult() {
List<Thing> things = new ArrayList<Thing>();
when(thing.getThings()).thenReturn(things);
Thing thing = mock(Thing.class);
things.add(thing);
BoschSHCHandler thingHandler = mock(BoschSHCHandler.class);
when(thing.getHandler()).thenReturn(thingHandler);
when(thingHandler.getBoschID()).thenReturn("hdm:HomeMaticIP:3014F711A0001916D859A8A9");
String json = """
{
"result": [{
"path": "/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch",
"@type": "DeviceServiceData",
"id": "PowerSwitch",
"state": {
"@type": "powerSwitchState",
"switchState": "ON"
},
"deviceId": "hdm:HomeMaticIP:3014F711A0001916D859A8A9"
}],
"jsonrpc": "2.0"
}
""";
LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, LongPollResult.class);
assertNotNull(longPollResult);
fixture.handleLongPollResult(longPollResult);
verify(thingHandler).getBoschID();
JsonElement expectedState = JsonParser.parseString("""
{
"@type": "powerSwitchState",
"switchState": "ON"
}
""");
verify(thingHandler).processUpdate("PowerSwitch", expectedState);
}
@Test
void handleLongPollResultHandleChildUpdate() {
List<Thing> things = new ArrayList<Thing>();
when(thing.getThings()).thenReturn(things);
Thing thing = mock(Thing.class);
things.add(thing);
BoschSHCHandler thingHandler = mock(BoschSHCHandler.class);
when(thing.getHandler()).thenReturn(thingHandler);
when(thingHandler.getBoschID()).thenReturn("hdm:ZigBee:70ac08fffefead2d");
String json = """
{
"result": [{
"path": "/devices/hdm:ZigBee:70ac08fffefead2d#3/services/PowerSwitch",
"@type": "DeviceServiceData",
"id": "PowerSwitch",
"state": {
"@type": "powerSwitchState",
"switchState": "ON"
},
"deviceId": "hdm:ZigBee:70ac08fffefead2d#3"
}],
"jsonrpc": "2.0"
}
""";
LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, LongPollResult.class);
assertNotNull(longPollResult);
fixture.handleLongPollResult(longPollResult);
verify(thingHandler).getBoschID();
JsonElement expectedState = JsonParser.parseString("""
{
"@type": "powerSwitchState",
"switchState": "ON"
}
""");
verify(thingHandler).processChildUpdate("hdm:ZigBee:70ac08fffefead2d#3", "PowerSwitch", expectedState);
}
}

View File

@ -0,0 +1,284 @@
/**
* Copyright (c) 2010-2024 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.lightcontrol;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import javax.measure.quantity.Energy;
import javax.measure.quantity.Power;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.openhab.binding.boschshc.internal.devices.AbstractBoschSHCDeviceHandlerTest;
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.childprotection.dto.ChildProtectionServiceState;
import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchService;
import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState;
import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState;
import org.openhab.core.library.types.DecimalType;
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.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingTypeUID;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
/**
* Unit tests for {@link LightControl2Handler}.
*
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
class LightControl2HandlerTest extends AbstractBoschSHCDeviceHandlerTest<LightControl2Handler> {
private static final String CHILD_DEVICE_ID_1 = "hdm:ZigBee:70ac08fffefead2d#2";
private static final String CHILD_DEVICE_ID_2 = "hdm:ZigBee:70ac08fffefead2d#3";
private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Power>> powerCaptor;
private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Energy>> energyCaptor;
private @Captor @NonNullByDefault({}) ArgumentCaptor<ChildProtectionServiceState> childProtectionServiceStateCaptor;
private @Captor @NonNullByDefault({}) ArgumentCaptor<PowerSwitchServiceState> powerSwitchStateCaptor;
@Override
protected LightControl2Handler createFixture() {
return new LightControl2Handler(getThing());
}
@Override
protected ThingTypeUID getThingTypeUID() {
return BoschSHCBindingConstants.THING_TYPE_LIGHT_CONTROL_2;
}
@Override
protected String getDeviceID() {
return "hdm:ZigBee:70ac08fcfefa5197";
}
@Override
protected void configureDevice(Device device) {
super.configureDevice(device);
// order is reversed to test child ID sorting during initialization
device.childDeviceIds = List.of(CHILD_DEVICE_ID_2, CHILD_DEVICE_ID_1);
}
@Test
void testUpdateChannelCommunicationQualityService() {
String json = """
{
"@type": "communicationQualityState",
"quality": "UNKNOWN"
}
""";
JsonElement jsonObject = JsonParser.parseString(json);
getFixture().processUpdate("CommunicationQuality", jsonObject);
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_SIGNAL_STRENGTH),
new DecimalType(0));
json = """
{
"@type": "communicationQualityState",
"quality": "GOOD"
}
""";
jsonObject = JsonParser.parseString(json);
getFixture().processUpdate("CommunicationQuality", jsonObject);
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_SIGNAL_STRENGTH),
new DecimalType(4));
}
@Test
void testUpdateChannelPowerMeterServiceState() {
JsonElement jsonObject = JsonParser.parseString("""
{
"@type": "powerMeterState",
"powerConsumption": "23",
"energyConsumption": 42
}\
""");
getFixture().processUpdate("PowerMeter", jsonObject);
verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION)),
powerCaptor.capture());
QuantityType<Power> powerValue = powerCaptor.getValue();
assertEquals(23, powerValue.intValue());
verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION)),
energyCaptor.capture());
QuantityType<Energy> energyValue = energyCaptor.getValue();
assertEquals(42, energyValue.intValue());
}
@Test
void testHandleCommandPowerSwitchChannelChildDevice1()
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH_1), OnOffType.ON);
verify(getBridgeHandler()).putState(eq(CHILD_DEVICE_ID_1), eq("PowerSwitch"), powerSwitchStateCaptor.capture());
PowerSwitchServiceState state = powerSwitchStateCaptor.getValue();
assertSame(PowerSwitchState.ON, state.switchState);
getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH_1), OnOffType.OFF);
verify(getBridgeHandler(), times(2)).putState(eq(CHILD_DEVICE_ID_1), eq("PowerSwitch"),
powerSwitchStateCaptor.capture());
state = powerSwitchStateCaptor.getValue();
assertSame(PowerSwitchState.OFF, state.switchState);
}
@Test
void testHandleCommandPowerSwitchChannelChildDevice2()
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH_2), OnOffType.ON);
verify(getBridgeHandler()).putState(eq(CHILD_DEVICE_ID_2), eq("PowerSwitch"), powerSwitchStateCaptor.capture());
PowerSwitchServiceState state = powerSwitchStateCaptor.getValue();
assertSame(PowerSwitchState.ON, state.switchState);
getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH_2), OnOffType.OFF);
verify(getBridgeHandler(), times(2)).putState(eq(CHILD_DEVICE_ID_2), eq("PowerSwitch"),
powerSwitchStateCaptor.capture());
state = powerSwitchStateCaptor.getValue();
assertSame(PowerSwitchState.OFF, state.switchState);
}
@Test
void testUpdateChannelPowerSwitchStateChildDevice1() {
JsonElement jsonObject = JsonParser
.parseString("{\n" + " \"@type\": \"powerSwitchState\",\n" + " \"switchState\": \"ON\"\n" + "}");
getFixture().processChildUpdate(CHILD_DEVICE_ID_1, PowerSwitchService.POWER_SWITCH_SERVICE_NAME, jsonObject);
verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH_1),
OnOffType.ON);
jsonObject = JsonParser
.parseString("{\n" + " \"@type\": \"powerSwitchState\",\n" + " \"switchState\": \"OFF\"\n" + "}");
getFixture().processChildUpdate(CHILD_DEVICE_ID_1, PowerSwitchService.POWER_SWITCH_SERVICE_NAME, jsonObject);
verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH_1),
OnOffType.OFF);
}
@Test
void testUpdateChannelPowerSwitchStateChildDevice2() {
JsonElement jsonObject = JsonParser
.parseString("{\n" + " \"@type\": \"powerSwitchState\",\n" + " \"switchState\": \"ON\"\n" + "}");
getFixture().processChildUpdate(CHILD_DEVICE_ID_2, PowerSwitchService.POWER_SWITCH_SERVICE_NAME, jsonObject);
verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH_2),
OnOffType.ON);
jsonObject = JsonParser
.parseString("{\n" + " \"@type\": \"powerSwitchState\",\n" + " \"switchState\": \"OFF\"\n" + "}");
getFixture().processChildUpdate(CHILD_DEVICE_ID_2, PowerSwitchService.POWER_SWITCH_SERVICE_NAME, jsonObject);
verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH_2),
OnOffType.OFF);
}
@Test
void testUpdateChannelsChildProtectionServiceChildDevice1() {
String json = """
{
"@type": "ChildProtectionState",
"childLockActive": true
}
""";
JsonElement jsonObject = JsonParser.parseString(json);
getFixture().processChildUpdate(CHILD_DEVICE_ID_1, "ChildProtection", jsonObject);
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION_1), OnOffType.ON);
}
@Test
void testUpdateChannelsChildProtectionServiceChildDevice2() {
String json = """
{
"@type": "ChildProtectionState",
"childLockActive": true
}
""";
JsonElement jsonObject = JsonParser.parseString(json);
getFixture().processChildUpdate(CHILD_DEVICE_ID_2, "ChildProtection", jsonObject);
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION_2), OnOffType.ON);
}
@Test
void testHandleCommandChildProtectionServiceChildDevice1()
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
getFixture().handleCommand(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION_1), OnOffType.ON);
verify(getBridgeHandler()).putState(eq(CHILD_DEVICE_ID_1), eq("ChildProtection"),
childProtectionServiceStateCaptor.capture());
ChildProtectionServiceState state = childProtectionServiceStateCaptor.getValue();
assertTrue(state.childLockActive);
}
@Test
void testHandleCommandChildProtectionServiceChildDevice2()
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
getFixture().handleCommand(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION_2), OnOffType.ON);
verify(getBridgeHandler()).putState(eq(CHILD_DEVICE_ID_2), eq("ChildProtection"),
childProtectionServiceStateCaptor.capture());
ChildProtectionServiceState state = childProtectionServiceStateCaptor.getValue();
assertTrue(state.childLockActive);
}
@Test
void testInitializeNoChildIDsInDeviceInfo() {
getDevice().childDeviceIds = null;
getFixture().initialize();
verify(getCallback()).statusUpdated(same(getThing()),
argThat(status -> status.getStatus().equals(ThingStatus.OFFLINE)
&& status.getStatusDetail().equals(ThingStatusDetail.CONFIGURATION_ERROR)));
}
@Test
void testInitializeServicesNoChildIDsInDeviceInfo() {
getDevice().childDeviceIds = null;
LightControl2Handler lFixture = new LightControl2Handler(getThing());
lFixture.setCallback(getCallback());
// this call will return before reaching initializeServices()
lFixture.initialize();
assertThrows(BoschSHCException.class, () -> lFixture.initializeServices());
}
}

View File

@ -13,7 +13,7 @@
package org.openhab.binding.boschshc.internal.devices.lightcontrol;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerTest;
import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerWithPowerMeterTest;
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.core.thing.ThingTypeUID;
@ -24,7 +24,7 @@ import org.openhab.core.thing.ThingTypeUID;
*
*/
@NonNullByDefault
public class LightControlHandlerTest extends AbstractPowerSwitchHandlerTest<LightControlHandler> {
class LightControlHandlerTest extends AbstractPowerSwitchHandlerWithPowerMeterTest<LightControlHandler> {
@Override
protected ThingTypeUID getThingTypeUID() {

View File

@ -13,7 +13,7 @@
package org.openhab.binding.boschshc.internal.devices.plug;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerTest;
import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerWithPowerMeterTest;
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.core.thing.ThingTypeUID;
@ -24,7 +24,7 @@ import org.openhab.core.thing.ThingTypeUID;
*
*/
@NonNullByDefault
public class PlugHandlerTest extends AbstractPowerSwitchHandlerTest<PlugHandler> {
class PlugHandlerTest extends AbstractPowerSwitchHandlerWithPowerMeterTest<PlugHandler> {
@Override
protected PlugHandler createFixture() {

View File

@ -0,0 +1,143 @@
/**
* Copyright (c) 2010-2024 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.shuttercontrol;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import javax.measure.quantity.Energy;
import javax.measure.quantity.Power;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.childprotection.dto.ChildProtectionServiceState;
import org.openhab.core.library.types.DecimalType;
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 com.google.gson.JsonElement;
import com.google.gson.JsonParser;
/**
* Unit tests for {@link ShutterControl2Handler}
*
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
class ShutterControl2HandlerTest extends ShutterControlHandlerTest {
private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Power>> powerCaptor;
private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Energy>> energyCaptor;
private @Captor @NonNullByDefault({}) ArgumentCaptor<ChildProtectionServiceState> childProtectionServiceStateCaptor;
@Override
protected ShutterControlHandler createFixture() {
return new ShutterControl2Handler(getThing());
}
@Override
protected ThingTypeUID getThingTypeUID() {
return BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL_2;
}
@Test
void testUpdateChannelsCommunicationQualityService() {
String json = """
{
"@type": "communicationQualityState",
"quality": "UNKNOWN"
}
""";
JsonElement jsonObject = JsonParser.parseString(json);
getFixture().processUpdate("CommunicationQuality", jsonObject);
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_SIGNAL_STRENGTH),
new DecimalType(0));
json = """
{
"@type": "communicationQualityState",
"quality": "GOOD"
}
""";
jsonObject = JsonParser.parseString(json);
getFixture().processUpdate("CommunicationQuality", jsonObject);
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_SIGNAL_STRENGTH),
new DecimalType(4));
}
@Test
void testUpdateChannelsChildProtectionService() {
String json = """
{
"@type": "ChildProtectionState",
"childLockActive": true
}
""";
JsonElement jsonObject = JsonParser.parseString(json);
getFixture().processUpdate("ChildProtection", jsonObject);
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION), OnOffType.ON);
}
@Test
void testUpdateChannelPowerMeterServiceState() {
JsonElement jsonObject = JsonParser.parseString("""
{
"@type": "powerMeterState",
"powerConsumption": "23",
"energyConsumption": 42
}\
""");
getFixture().processUpdate("PowerMeter", jsonObject);
verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION)),
powerCaptor.capture());
QuantityType<Power> powerValue = powerCaptor.getValue();
assertEquals(23, powerValue.intValue());
verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION)),
energyCaptor.capture());
QuantityType<Energy> energyValue = energyCaptor.getValue();
assertEquals(42, energyValue.intValue());
}
@Test
void testHandleCommandChildProtection()
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
getFixture().handleCommand(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION), OnOffType.ON);
verify(getBridgeHandler()).putState(eq(getDeviceID()), eq("ChildProtection"),
childProtectionServiceStateCaptor.capture());
ChildProtectionServiceState state = childProtectionServiceStateCaptor.getValue();
assertTrue(state.childLockActive);
}
}

View File

@ -13,10 +13,16 @@
package org.openhab.binding.boschshc.internal.discovery;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.UUID;
@ -41,7 +47,7 @@ import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingUID;
/**
* ThingDiscoveryService Tester.
* Unit tests for {@link ThingDiscoveryService}.
*
* @author Gerd Zanker - Initial contribution
*/
@ -261,4 +267,12 @@ class ThingDiscoveryServiceTest {
// two calls for the two devices expected
verify(discoveryListener, times(2)).thingDiscovered(any(), any());
}
@Test
void getThingTypeUIDLightControl2ChildDevice() {
Device device = new Device();
device.deviceModel = ThingDiscoveryService.DEVICE_MODEL_LIGHT_CONTROL_CHILD_DEVICE;
assertThat(fixture.getThingTypeUID(device), is(nullValue()));
}
}