[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 # Bosch Smart Home Binding
Binding for the Bosch Smart Home. Binding for Bosch Smart Home devices.
- [Bosch Smart Home Binding](#bosch-smart-home-binding) - [Bosch Smart Home Binding](#bosch-smart-home-binding)
- [Supported Things](#supported-things) - [Supported Things](#supported-things)
@ -10,8 +10,10 @@ Binding for the Bosch Smart Home.
- [Twinguard Smoke Detector](#twinguard-smoke-detector) - [Twinguard Smoke Detector](#twinguard-smoke-detector)
- [Door/Window Contact](#door-window-contact) - [Door/Window Contact](#door-window-contact)
- [Door/Window Contact II](#door-window-contact-ii) - [Door/Window Contact II](#door-window-contact-ii)
- [Light Control II](#light-control-ii)
- [Motion Detector](#motion-detector) - [Motion Detector](#motion-detector)
- [Shutter Control](#shutter-control) - [Shutter Control](#shutter-control)
- [Shutter Control II](#shutter-control-ii)
- [Thermostat](#thermostat) - [Thermostat](#thermostat)
- [Climate Control](#climate-control) - [Climate Control](#climate-control)
- [Wall Thermostat](#wall-thermostat) - [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. | | 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). | | 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 ### Motion Detector
Detects every movement through an intelligent combination of passive infra-red technology and an additional temperature sensor. 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). | | 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 ### Thermostat
Radiator thermostat Radiator thermostat

View File

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

View File

@ -12,20 +12,16 @@
*/ */
package org.openhab.binding.boschshc.internal.devices; 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 java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; 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.PowerSwitchService;
import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState; import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState;
import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState; import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState;
import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
@ -61,8 +57,6 @@ public abstract class AbstractPowerSwitchHandler extends BoschSHCDeviceHandler {
super.initializeServices(); super.initializeServices();
this.registerService(this.powerSwitchService, this::updateChannels, List.of(CHANNEL_POWER_SWITCH), true); 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 @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}. * @param state the new {@link PowerSwitchService} state.
*/
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}.
*/ */
private void updateChannels(PowerSwitchServiceState state) { private void updateChannels(PowerSwitchServiceState state) {
State powerState = OnOffType.from(state.switchState.toString()); 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_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_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 = 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_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_CLIMATE_CONTROL = new ThingTypeUID(BINDING_ID, "climate-control");
public static final ThingTypeUID THING_TYPE_WALL_THERMOSTAT = new ThingTypeUID(BINDING_ID, "wall-thermostat"); 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_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 = 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_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_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 // List of all Channel IDs
// Auto-generated from thing-types.xml via script, don't modify // 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_VALVE_TAPPET_POSITION = "valve-tappet-position";
public static final String CHANNEL_SETPOINT_TEMPERATURE = "setpoint-temperature"; public static final String CHANNEL_SETPOINT_TEMPERATURE = "setpoint-temperature";
public static final String CHANNEL_CHILD_LOCK = "child-lock"; 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_PRIVACY_MODE = "privacy-mode";
public static final String CHANNEL_CAMERA_NOTIFICATION = "camera-notification"; public static final String CHANNEL_CAMERA_NOTIFICATION = "camera-notification";
public static final String CHANNEL_SYSTEM_AVAILABILITY = "system-availability"; 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_TYPE = "key-event-type";
public static final String CHANNEL_KEY_EVENT_TIMESTAMP = "key-event-timestamp"; 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"; public static final String CHANNEL_USER_DEFINED_STATE = "user-state";
// static device/service names // 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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; 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.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatus;
@ -57,30 +58,68 @@ public abstract class BoschSHCDeviceHandler extends BoschSHCHandler {
@Override @Override
public void initialize() { public void initialize() {
var config = this.config = getConfigAs(BoschSHCConfiguration.class); this.config = getConfigAs(BoschSHCConfiguration.class);
String deviceId = config.id; 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()) { if (deviceId == null || deviceId.isBlank()) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.conf-error.empty-device-id"); "@text/offline.conf-error.empty-device-id");
return; return null;
} }
// Try to get device info to make sure the device exists // Try to get device info to make sure the device exists
try { try {
var bridgeHandler = this.getBridgeHandler(); var bridgeHandler = this.getBridgeHandler();
var info = bridgeHandler.getDeviceInfo(deviceId); var deviceInfo = bridgeHandler.getDeviceInfo(deviceId);
logger.trace("Device initialized:\n{}", info); logger.trace("Device validated and initialized:\n{}", deviceInfo);
return deviceInfo;
} catch (TimeoutException | ExecutionException | BoschSHCException e) { } catch (TimeoutException | ExecutionException | BoschSHCException e) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
return;
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); 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 Stefan Kästle - Initial contribution
* @author Christian Oeing - refactorings of e.g. server registration * @author Christian Oeing - refactorings of e.g. server registration
* @author David Pace - Handler abstraction * @author David Pace - Handler abstraction
* @author David Pace - Support for child device updates
*/ */
@NonNullByDefault @NonNullByDefault
public abstract class BoschSHCHandler extends BaseThingHandler { 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. * @param stateData Current state of device service. Serialized as JSON.
*/ */
public void processUpdate(String serviceName, @Nullable JsonElement stateData) { 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) { for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
BoschSHCService<? extends BoschSHCServiceState> service = deviceService.service; BoschSHCService<? extends BoschSHCServiceState> service = deviceService.service;
if (serviceName.equals(service.getServiceName())) { 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 * Use this method to register all services of the device with
* {@link #registerService(BoschSHCService, Consumer, Collection, boolean)}. * {@link #registerService(BoschSHCService, Consumer, Collection, boolean)}.
*/ */
protected void initializeServices() throws BoschSHCException { 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_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_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_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_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_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;
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_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_SMART_PLUG_COMPACT;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SMOKE_DETECTOR; 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.camera.CameraHandler;
import org.openhab.binding.boschshc.internal.devices.climatecontrol.ClimateControlHandler; 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.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.lightcontrol.LightControlHandler;
import org.openhab.binding.boschshc.internal.devices.motiondetector.MotionDetectorHandler; 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.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.shuttercontrol.ShutterControlHandler;
import org.openhab.binding.boschshc.internal.devices.smartbulb.SmartBulbHandler; import org.openhab.binding.boschshc.internal.devices.smartbulb.SmartBulbHandler;
import org.openhab.binding.boschshc.internal.devices.smokedetector.SmokeDetector2Handler; 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_WINDOW_CONTACT_2, WindowContact2Handler::new),
new ThingTypeHandlerMapping(THING_TYPE_MOTION_DETECTOR, MotionDetectorHandler::new), new ThingTypeHandlerMapping(THING_TYPE_MOTION_DETECTOR, MotionDetectorHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_SHUTTER_CONTROL, ShutterControlHandler::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_THERMOSTAT, ThermostatHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_CLIMATE_CONTROL, ClimateControlHandler::new), new ThingTypeHandlerMapping(THING_TYPE_CLIMATE_CONTROL, ClimateControlHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_WALL_THERMOSTAT, WallThermostatHandler::new), new ThingTypeHandlerMapping(THING_TYPE_WALL_THERMOSTAT, WallThermostatHandler::new),
@ -123,7 +128,8 @@ public class BoschSHCHandlerFactory extends BaseThingHandlerFactory {
thing -> new UniversalSwitchHandler(thing, timeZoneProvider)), thing -> new UniversalSwitchHandler(thing, timeZoneProvider)),
new ThingTypeHandlerMapping(THING_TYPE_UNIVERSAL_SWITCH_2, new ThingTypeHandlerMapping(THING_TYPE_UNIVERSAL_SWITCH_2,
thing -> new UniversalSwitch2Handler(thing, timeZoneProvider)), 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 @Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) { public boolean supportsThingType(ThingTypeUID thingTypeUID) {

View File

@ -111,7 +111,32 @@ public class BoschHttpClient extends HttpClient {
* @return Bosch SHC URL for passed endpoint * @return Bosch SHC URL for passed endpoint
*/ */
public String getBoschShcUrl(String 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.client.api.Response;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.boschshc.internal.devices.BoschDeviceIdUtils;
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants; import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler; import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device; import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
@ -476,7 +477,7 @@ public class BridgeHandler extends BaseBridgeHandler {
* *
* @param result Results from Long Polling * @param result Results from Long Polling
*/ */
private void handleLongPollResult(LongPollResult result) { void handleLongPollResult(LongPollResult result) {
for (BoschSHCServiceState serviceState : result.result) { for (BoschSHCServiceState serviceState : result.result) {
if (serviceState instanceof DeviceServiceData deviceServiceData) { if (serviceState instanceof DeviceServiceData deviceServiceData) {
handleDeviceServiceData(deviceServiceData); handleDeviceServiceData(deviceServiceData);
@ -562,12 +563,7 @@ public class BridgeHandler extends BaseBridgeHandler {
*/ */
private void forwardStateToHandlers(BoschSHCServiceState serviceData, JsonElement state, String updateDeviceId) { private void forwardStateToHandlers(BoschSHCServiceState serviceData, JsonElement state, String updateDeviceId) {
boolean handled = false; boolean handled = false;
final String serviceId; final String serviceId = getServiceId(serviceData);
if (serviceData instanceof UserDefinedState userState) {
serviceId = userState.getId();
} else {
serviceId = ((DeviceServiceData) serviceData).id;
}
Bridge bridge = this.getThing(); Bridge bridge = this.getThing();
for (Thing childThing : bridge.getThings()) { for (Thing childThing : bridge.getThings()) {
@ -578,13 +574,17 @@ public class BridgeHandler extends BaseBridgeHandler {
@Nullable @Nullable
String deviceId = handler.getBoschID(); String deviceId = handler.getBoschID();
handled = true; if (deviceId == null) {
logger.debug("Registered device: {} - looking for {}", deviceId, updateDeviceId); continue;
if (deviceId != null && updateDeviceId.equals(deviceId)) {
logger.debug("Found child: {} - calling processUpdate (id: {}) with {}", handler, serviceId, state);
handler.processUpdate(serviceId, state);
} }
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 { } else {
logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler); 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. * 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. * Represents a single devices connected to the Bosch Smart Home Controller.
* * <p>
* Example from Json: * Example JSON:
* *
* <pre>
* { * {
* "@type":"device", * "@type": "device",
* "rootDeviceId":"64-da-a0-02-14-9b", * "rootDeviceId": "64-da-a0-02-14-9b",
* "id":"hdm:HomeMaticIP:3014F711A00004953859F31B", * "id": "hdm:HomeMaticIP:3014F711A00004953859F31B",
* "deviceServiceIds":["PowerMeter","PowerSwitch","PowerSwitchProgram","Routing"], * "deviceServiceIds": ["PowerMeter","PowerSwitch","PowerSwitchProgram","Routing"],
* "manufacturer":"BOSCH", * "manufacturer": "BOSCH",
* "roomId":"hz_3", * "roomId": "hz_3",
* "deviceModel":"PSM", * "deviceModel": "PSM",
* "serial":"3014F711A00004953859F31B", * "serial": "3014F711A00004953859F31B",
* "profile":"GENERIC", * "profile": "GENERIC",
* "name":"Coffee Machine", * "name": "Coffee Machine",
* "status":"AVAILABLE", * "status": "AVAILABLE",
* "childDeviceIds":[] * "childDeviceIds": []
* } * }
* </pre>
* *
* @author Stefan Kästle - Initial contribution * @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. * 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 * @author Stefan Kästle - Initial contribution
*/ */
public class LongPollResult { 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 ArrayList<BoschSHCServiceState> result;
public String jsonrpc; 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; package org.openhab.binding.boschshc.internal.devices.lightcontrol;
import org.eclipse.jdt.annotation.NonNullByDefault; 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; import org.openhab.core.thing.Thing;
/** /**
@ -22,7 +22,7 @@ import org.openhab.core.thing.Thing;
* @author Stefan Kästle - Initial contribution * @author Stefan Kästle - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public class LightControlHandler extends AbstractPowerSwitchHandler { public class LightControlHandler extends AbstractPowerSwitchHandlerWithPowerMeter {
public LightControlHandler(Thing thing) { public LightControlHandler(Thing thing) {
super(thing); super(thing);

View File

@ -13,7 +13,7 @@
package org.openhab.binding.boschshc.internal.devices.plug; package org.openhab.binding.boschshc.internal.devices.plug;
import org.eclipse.jdt.annotation.NonNullByDefault; 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; import org.openhab.core.thing.Thing;
/** /**
@ -22,7 +22,7 @@ import org.openhab.core.thing.Thing;
* @author David Pace - Initial contribution * @author David Pace - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public class PlugHandler extends AbstractPowerSwitchHandler { public class PlugHandler extends AbstractPowerSwitchHandlerWithPowerMeter {
public PlugHandler(Thing thing) { public PlugHandler(Thing thing) {
super(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); 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( protected static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(
BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH, BoschSHCBindingConstants.THING_TYPE_TWINGUARD, BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH, BoschSHCBindingConstants.THING_TYPE_TWINGUARD,
BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT, BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT_2, 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<>("TRV", BoschSHCBindingConstants.THING_TYPE_THERMOSTAT),
new AbstractMap.SimpleEntry<>("WRC2", BoschSHCBindingConstants.THING_TYPE_UNIVERSAL_SWITCH), new AbstractMap.SimpleEntry<>("WRC2", BoschSHCBindingConstants.THING_TYPE_UNIVERSAL_SWITCH),
new AbstractMap.SimpleEntry<>("SWITCH2", BoschSHCBindingConstants.THING_TYPE_UNIVERSAL_SWITCH_2), 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 // Future Extension: map deviceModel names to BoschSHC Thing Types when they are supported
// new AbstractMap.SimpleEntry<>("SMOKE_DETECTION_SYSTEM", BoschSHCBindingConstants.), // new AbstractMap.SimpleEntry<>("SMOKE_DETECTION_SYSTEM", BoschSHCBindingConstants.),
// new AbstractMap.SimpleEntry<>("PRESENCE_SIMULATION_SERVICE", 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); 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); logger.trace("- got thingUID '{}' for device: '{}'", thingUID, device);
DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID) DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID)
.withProperty("id", device.id).withLabel(getNiceName(device.name, roomName)); .withProperty("id", device.id).withLabel(getNiceName(device.name, roomName));
discoveryResult.withBridge(thingHandler.getThing().getUID()); discoveryResult.withBridge(thingHandler.getThing().getUID());
if (!roomName.isEmpty()) { if (!roomName.isEmpty()) {
discoveryResult.withProperty("Location", roomName); discoveryResult.withProperty("Location", roomName);
} }
@ -235,6 +245,18 @@ public class ThingDiscoveryService extends AbstractThingHandlerDiscoveryService<
thingUID, thingTypeUID, device.id, device.deviceModel); 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) { private String getNiceName(String name, String roomName) {
if (!name.startsWith("-")) { if (!name.startsWith("-")) {
return name; return name;
@ -268,6 +290,15 @@ public class ThingDiscoveryService extends AbstractThingHandlerDiscoveryService<
if (thingTypeId != null) { if (thingTypeId != null) {
return new ThingTypeUID(BoschSHCBindingConstants.BINDING_ID, thingTypeId.getId()); 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.", logger.debug("Unknown deviceModel '{}'! Please create a support request issue for this unknown device model.",
device.deviceModel); device.deviceModel);
return null; 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 @NonNullByDefault
public class PowerSwitchService extends BoschSHCService<PowerSwitchServiceState> { public class PowerSwitchService extends BoschSHCService<PowerSwitchServiceState> {
public static final String POWER_SWITCH_SERVICE_NAME = "PowerSwitch";
public PowerSwitchService() { 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:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0" 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"> 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"> <config-description uri="thing-type:boschshc:bridge">
<parameter name="ipAddress" type="text" required="true"> <parameter name="ipAddress" type="text" required="true">
<context>network-address</context> <context>network-address</context>
@ -15,16 +16,19 @@
<description>The system password of the Bosch Smart Home Controller necessary for pairing.</description> <description>The system password of the Bosch Smart Home Controller necessary for pairing.</description>
</parameter> </parameter>
</config-description> </config-description>
<config-description uri="thing-type:boschshc:device"> <config-description uri="thing-type:boschshc:device">
<parameter name="id" type="text" required="true"> <parameter name="id" type="text" required="true">
<label>Device ID</label> <label>Device ID</label>
<description>Unique ID of the device.</description> <description>Unique ID of the device.</description>
</parameter> </parameter>
</config-description> </config-description>
<config-description uri="thing-type:boschshc:user-defined-state"> <config-description uri="thing-type:boschshc:user-defined-state">
<parameter name="id" type="text" required="true"> <parameter name="id" type="text" required="true">
<label>State ID</label> <label>State ID</label>
<description>Unique ID of the state.</description> <description>Unique ID of the state.</description>
</parameter> </parameter>
</config-description> </config-description>
</config-description:config-descriptions> </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.in-wall-switch.description = A simple light control.
thing-type.boschshc.intrusion-detection-system.label = Intrusion Detection System 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.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.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.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 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.security-camera-eyes.description = Outdoor security camera with motion detection and light.
thing-type.boschshc.shc.label = Smart Home Controller 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.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.label = Shutter Control
thing-type.boschshc.shutter-control.description = Control of your shutter to take any position you desire. thing-type.boschshc.shutter-control.description = Control of your shutter to take any position you desire.
thing-type.boschshc.smart-bulb.label = Smart Bulb 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.camera-notification.state.option.OFF = Disabled
channel-type.boschshc.child-lock.label = Child Lock 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-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.label = Combined Rating
channel-type.boschshc.combined-rating.description = Combined rating of the air quality. channel-type.boschshc.combined-rating.description = Combined rating of the air quality.
channel-type.boschshc.combined-rating.state.option.GOOD = Good 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.invalid-device-id = Device ID is invalid.
offline.conf-error.empty-state-id = No ID set. offline.conf-error.empty-state-id = No ID set.
offline.conf-error.invalid-state-id = ID is invalid. 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>
<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"> <thing-type id="thermostat">
<supported-bridge-type-refs> <supported-bridge-type-refs>
<bridge-type-ref id="shc"/> <bridge-type-ref id="shc"/>
@ -703,4 +745,10 @@
<state readOnly="true"/> <state readOnly="true"/>
</channel-type> </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> </thing:thing-descriptions>

View File

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

View File

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

View File

@ -12,28 +12,26 @@
*/ */
package org.openhab.binding.boschshc.internal.devices; package org.openhab.binding.boschshc.internal.devices;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.ArgumentMatchers.*; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*; 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.ExecutionException;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import javax.measure.quantity.Energy;
import javax.measure.quantity.Power;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Captor; import org.mockito.Captor;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; 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.PowerSwitchState;
import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState; import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState;
import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.types.RefreshType; import org.openhab.core.types.RefreshType;
import com.google.gson.JsonElement; 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<PowerSwitchServiceState> serviceStateCaptor;
private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Power>> powerCaptor;
private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Energy>> energyCaptor;
@BeforeEach @BeforeEach
@Override @Override
public void beforeEach() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { public void beforeEach() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
@ -65,12 +59,6 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
powerSwitchServiceState.switchState = PowerSwitchState.ON; powerSwitchServiceState.switchState = PowerSwitchState.ON;
lenient().when(bridgeHandler.getState(anyString(), eq("PowerSwitch"), same(PowerSwitchServiceState.class))) lenient().when(bridgeHandler.getState(anyString(), eq("PowerSwitch"), same(PowerSwitchServiceState.class)))
.thenReturn(powerSwitchServiceState); .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 @Test
@ -101,47 +89,9 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.OFF); 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 @Test
public void testHandleCommandRefreshPowerSwitchChannel() { public void testHandleCommandRefreshPowerSwitchChannel() {
getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), RefreshType.REFRESH); getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), RefreshType.REFRESH);
verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.ON); 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)); 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 @Test
void isAccessPossible() throws InterruptedException { void isAccessPossible() throws InterruptedException {
assertFalse(httpClient.isAccessPossible()); assertFalse(httpClient.isAccessPossible());

View File

@ -12,14 +12,27 @@
*/ */
package org.openhab.binding.boschshc.internal.devices.bridge; package org.openhab.binding.boschshc.internal.devices.bridge;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.*; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.*; 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.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; 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.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; 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.Device;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData; 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.DeviceTest;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Faults; 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.SubscribeResult;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState; import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedStateTest; import org.openhab.binding.boschshc.internal.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.ThingHandlerCallback;
import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder; import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
/** /**
* Unit tests for the {@link BridgeHandler}. * Unit tests for the {@link BridgeHandler}.
* *
@ -77,6 +95,11 @@ class BridgeHandlerTest {
private @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback; private @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
/**
* A mocked bridge instance
*/
private @NonNullByDefault({}) Bridge thing;
@BeforeAll @BeforeAll
static void beforeAll() throws IOException { static void beforeAll() throws IOException {
Path mavenTargetFolder = Paths.get("target"); Path mavenTargetFolder = Paths.get("target");
@ -102,7 +125,7 @@ class BridgeHandlerTest {
properties.put("password", "test"); properties.put("password", "test");
bridgeConfiguration.setProperties(properties); bridgeConfiguration.setProperties(properties);
Thing thing = mock(Bridge.class); thing = mock(Bridge.class);
when(thing.getConfiguration()).thenReturn(bridgeConfiguration); when(thing.getConfiguration()).thenReturn(bridgeConfiguration);
// this calls initialize() as well // this calls initialize() as well
fixture.thingUpdated(thing); fixture.thingUpdated(thing);
@ -502,4 +525,129 @@ class BridgeHandlerTest {
void afterEach() throws Exception { void afterEach() throws Exception {
fixture.dispose(); 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; package org.openhab.binding.boschshc.internal.devices.lightcontrol;
import org.eclipse.jdt.annotation.NonNullByDefault; 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.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingTypeUID;
@ -24,7 +24,7 @@ import org.openhab.core.thing.ThingTypeUID;
* *
*/ */
@NonNullByDefault @NonNullByDefault
public class LightControlHandlerTest extends AbstractPowerSwitchHandlerTest<LightControlHandler> { class LightControlHandlerTest extends AbstractPowerSwitchHandlerWithPowerMeterTest<LightControlHandler> {
@Override @Override
protected ThingTypeUID getThingTypeUID() { protected ThingTypeUID getThingTypeUID() {

View File

@ -13,7 +13,7 @@
package org.openhab.binding.boschshc.internal.devices.plug; package org.openhab.binding.boschshc.internal.devices.plug;
import org.eclipse.jdt.annotation.NonNullByDefault; 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.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingTypeUID;
@ -24,7 +24,7 @@ import org.openhab.core.thing.ThingTypeUID;
* *
*/ */
@NonNullByDefault @NonNullByDefault
public class PlugHandlerTest extends AbstractPowerSwitchHandlerTest<PlugHandler> { class PlugHandlerTest extends AbstractPowerSwitchHandlerWithPowerMeterTest<PlugHandler> {
@Override @Override
protected PlugHandler createFixture() { 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; package org.openhab.binding.boschshc.internal.discovery;
import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat; 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.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.ArrayList;
import java.util.UUID; import java.util.UUID;
@ -41,7 +47,7 @@ import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.ThingUID;
/** /**
* ThingDiscoveryService Tester. * Unit tests for {@link ThingDiscoveryService}.
* *
* @author Gerd Zanker - Initial contribution * @author Gerd Zanker - Initial contribution
*/ */
@ -261,4 +267,12 @@ class ThingDiscoveryServiceTest {
// two calls for the two devices expected // two calls for the two devices expected
verify(discoveryListener, times(2)).thingDiscovered(any(), any()); 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()));
}
} }