# Bosch Smart Home Binding
-Binding for the Bosch Smart Home.
+Binding for Bosch Smart Home devices.
- [Bosch Smart Home Binding](#bosch-smart-home-binding)
- [Supported Things](#supported-things)
- [Twinguard Smoke Detector](#twinguard-smoke-detector)
- [Door/Window Contact](#door-window-contact)
- [Door/Window Contact II](#door-window-contact-ii)
+ - [Light Control II](#light-control-ii)
- [Motion Detector](#motion-detector)
- [Shutter Control](#shutter-control)
+ - [Shutter Control II](#shutter-control-ii)
- [Thermostat](#thermostat)
- [Climate Control](#climate-control)
- [Wall Thermostat](#wall-thermostat)
| bypass | Switch | ☐ | Indicates whether the device is currently bypassed. Possible values are `ON`,`OFF` and `UNDEF` if the bypass state cannot be determined. |
| signal-strength | Number | ☐ | 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 | ☐ | 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 | ☐ | Current power consumption (W) of the device. |
+| energy-consumption | Number:Energy | ☐ | Cumulated energy consumption (Wh) of the device. |
+| power-switch-1 | Switch | ☑ | Switches the light on or off (circuit 1). |
+| child-protection-1 | Switch | ☑ | Indicates whether the child protection is active (circuit 1). |
+| power-switch-2 | Switch | ☑ | Switches the light on or off (circuit 2). |
+| child-protection-2 | Switch | ☑ | Indicates whether the child protection is active (circuit 2). |
+
### Motion Detector
Detects every movement through an intelligent combination of passive infra-red technology and an additional temperature sensor.
| --------------- | ------------- | :------: | ---------------------------------------- |
| level | Rollershutter | ☑ | 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 | ☑ | Current open ratio (0 to 100, Step 0.5). |
+| signal-strength | Number | ☐ | Communication quality between the device and the Smart Home Controller. Possible values range between 0 (unknown) and 4 (best signal strength). |
+| child-protection | Switch | ☑ | Indicates whether the child protection is active. |
+| power-consumption | Number:Power | ☐ | Current power consumption (W) of the device. |
+| energy-consumption | Number:Energy | ☐ | Cumulated energy consumption (Wh) of the device. |
+
### Thermostat
Radiator thermostat
*/
List<String> getAllBoschShcServices() {
return List.of("airqualitylevel", "batterylevel", "binaryswitch", "bypass", "cameranotification", "childlock",
- "communicationquality", "hsbcoloractuator", "humiditylevel", "illuminance", "intrusion", "keypad",
- "latestmotion", "multilevelswitch", "powermeter", "powerswitch", "privacymode", "roomclimatecontrol",
- "shuttercontact", "shuttercontrol", "silentmode", "smokedetectorcheck", "temperaturelevel", "userstate",
- "valvetappet");
+ "childprotection", "communicationquality", "hsbcoloractuator", "humiditylevel", "illuminance",
+ "intrusion", "keypad", "latestmotion", "multilevelswitch", "powermeter", "powerswitch", "privacymode",
+ "roomclimatecontrol", "shuttercontact", "shuttercontrol", "silentmode", "smokedetectorcheck",
+ "temperaturelevel", "userstate", "valvetappet");
}
@Override
*/
package org.openhab.binding.boschshc.internal.devices;
-import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_POWER_SWITCH;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
-import org.openhab.binding.boschshc.internal.services.powermeter.PowerMeterService;
-import org.openhab.binding.boschshc.internal.services.powermeter.dto.PowerMeterServiceState;
import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchService;
import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState;
import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState;
import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.QuantityType;
-import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
super.initializeServices();
this.registerService(this.powerSwitchService, this::updateChannels, List.of(CHANNEL_POWER_SWITCH), true);
- this.createService(PowerMeterService::new, this::updateChannels,
- List.of(CHANNEL_POWER_CONSUMPTION, CHANNEL_ENERGY_CONSUMPTION), true);
}
@Override
}
/**
- * Updates the channels which are linked to the {@link PowerMeterService} of the device.
+ * Updates the power switch channel when a new state is received.
*
- * @param state Current state of {@link PowerMeterService}.
- */
- private void updateChannels(PowerMeterServiceState state) {
- super.updateState(CHANNEL_POWER_CONSUMPTION, new QuantityType<>(state.powerConsumption, Units.WATT));
- super.updateState(CHANNEL_ENERGY_CONSUMPTION, new QuantityType<>(state.energyConsumption, Units.WATT_HOUR));
- }
-
- /**
- * Updates the channels which are linked to the {@link PowerSwitchService} of the device.
- *
- * @param state Current state of {@link PowerSwitchService}.
+ * @param state the new {@link PowerSwitchService} state.
*/
private void updateChannels(PowerSwitchServiceState state) {
State powerState = OnOffType.from(state.switchState.toString());
--- /dev/null
+/**
+ * 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));
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
public static final ThingTypeUID THING_TYPE_WINDOW_CONTACT_2 = new ThingTypeUID(BINDING_ID, "window-contact-2");
public static final ThingTypeUID THING_TYPE_MOTION_DETECTOR = new ThingTypeUID(BINDING_ID, "motion-detector");
public static final ThingTypeUID THING_TYPE_SHUTTER_CONTROL = new ThingTypeUID(BINDING_ID, "shutter-control");
+ public static final ThingTypeUID THING_TYPE_SHUTTER_CONTROL_2 = new ThingTypeUID(BINDING_ID, "shutter-control-2");
public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat");
public static final ThingTypeUID THING_TYPE_CLIMATE_CONTROL = new ThingTypeUID(BINDING_ID, "climate-control");
public static final ThingTypeUID THING_TYPE_WALL_THERMOSTAT = new ThingTypeUID(BINDING_ID, "wall-thermostat");
public static final ThingTypeUID THING_TYPE_SMOKE_DETECTOR = new ThingTypeUID(BINDING_ID, "smoke-detector");
public static final ThingTypeUID THING_TYPE_UNIVERSAL_SWITCH = new ThingTypeUID(BINDING_ID, "universal-switch");
public static final ThingTypeUID THING_TYPE_UNIVERSAL_SWITCH_2 = new ThingTypeUID(BINDING_ID, "universal-switch-2");
+ public static final ThingTypeUID THING_TYPE_SMOKE_DETECTOR_2 = new ThingTypeUID(BINDING_ID, "smoke-detector-2");
+ public static final ThingTypeUID THING_TYPE_LIGHT_CONTROL_2 = new ThingTypeUID(BINDING_ID, "light-control-2");
public static final ThingTypeUID THING_TYPE_USER_DEFINED_STATE = new ThingTypeUID(BINDING_ID, "user-defined-state");
- public static final ThingTypeUID THING_TYPE_SMOKE_DETECTOR_2 = new ThingTypeUID(BINDING_ID, "smoke-detector-2");
// List of all Channel IDs
// Auto-generated from thing-types.xml via script, don't modify
public static final String CHANNEL_VALVE_TAPPET_POSITION = "valve-tappet-position";
public static final String CHANNEL_SETPOINT_TEMPERATURE = "setpoint-temperature";
public static final String CHANNEL_CHILD_LOCK = "child-lock";
+ public static final String CHANNEL_CHILD_PROTECTION = "child-protection";
public static final String CHANNEL_PRIVACY_MODE = "privacy-mode";
public static final String CHANNEL_CAMERA_NOTIFICATION = "camera-notification";
public static final String CHANNEL_SYSTEM_AVAILABILITY = "system-availability";
public static final String CHANNEL_KEY_EVENT_TYPE = "key-event-type";
public static final String CHANNEL_KEY_EVENT_TIMESTAMP = "key-event-timestamp";
+ // numbered channels
+ // the rationale for introducing numbered channels was discussed in
+ // https://github.com/openhab/openhab-addons/pull/16400
+ public static final String CHANNEL_POWER_SWITCH_1 = "power-switch-1";
+ public static final String CHANNEL_POWER_SWITCH_2 = "power-switch-2";
+ public static final String CHANNEL_CHILD_PROTECTION_1 = "child-protection-1";
+ public static final String CHANNEL_CHILD_PROTECTION_2 = "child-protection-2";
+
public static final String CHANNEL_USER_DEFINED_STATE = "user-state";
// static device/service names
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
@Override
public void initialize() {
- var config = this.config = getConfigAs(BoschSHCConfiguration.class);
+ this.config = getConfigAs(BoschSHCConfiguration.class);
String deviceId = config.id;
+ @Nullable
+ Device deviceInfo = validateDeviceId(deviceId);
+ if (deviceInfo == null) {
+ return;
+ }
+
+ if (!processDeviceInfo(deviceInfo)) {
+ return;
+ }
+
+ super.initialize();
+ }
+
+ /**
+ * Allows the handler to process the device info that was obtained from a REST
+ * call to the Smart Home Controller at <code>/devices/{deviceId}</code>.
+ *
+ * @param deviceInfo the device info obtained from the controller, guaranteed to be non-null
+ * @return <code>true</code> if the device info is valid and the initialization should proceed, <code>false</code>
+ * otherwise
+ */
+ protected boolean processDeviceInfo(Device deviceInfo) {
+ return true;
+ }
+
+ /**
+ * Attempts to obtain information about the device with the specified ID via a REST call.
+ * <p>
+ * If the REST call is successful, the device ID is considered to be valid and the resulting {@link Device} object
+ * is returned.
+ * <p>
+ * If the device ID is not configured/empty or the REST call is not successful, the device ID is considered invalid
+ * and <code>null</code> is returned.
+ *
+ * @param deviceId the device ID to check
+ * @return the {@link Device} info object if the REST call was successful, <code>null</code> otherwise
+ */
+ @Nullable
+ protected Device validateDeviceId(@Nullable String deviceId) {
if (deviceId == null || deviceId.isBlank()) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.conf-error.empty-device-id");
- return;
+ return null;
}
// Try to get device info to make sure the device exists
try {
var bridgeHandler = this.getBridgeHandler();
- var info = bridgeHandler.getDeviceInfo(deviceId);
- logger.trace("Device initialized:\n{}", info);
+ var deviceInfo = bridgeHandler.getDeviceInfo(deviceId);
+ logger.trace("Device validated and initialized:\n{}", deviceInfo);
+ return deviceInfo;
} catch (TimeoutException | ExecutionException | BoschSHCException e) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
- return;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
- return;
}
- super.initialize();
+ return null;
}
/**
* @author Stefan Kästle - Initial contribution
* @author Christian Oeing - refactorings of e.g. server registration
* @author David Pace - Handler abstraction
+ * @author David Pace - Support for child device updates
*/
@NonNullByDefault
public abstract class BoschSHCHandler extends BaseThingHandler {
* @param stateData Current state of device service. Serialized as JSON.
*/
public void processUpdate(String serviceName, @Nullable JsonElement stateData) {
- // Check services of device to correctly
+ // Find service(s) with the specified name and propagate new state to them
for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
BoschSHCService<? extends BoschSHCServiceState> service = deviceService.service;
if (serviceName.equals(service.getServiceName())) {
}
}
+ /**
+ * Processes an update for a logical child device.
+ *
+ * @param childDeviceId the ID of the logical child device
+ * @param serviceName the name of the service this update is targeted at
+ * @param stateData the new service state serialized as JSON
+ */
+ public void processChildUpdate(String childDeviceId, String serviceName, @Nullable JsonElement stateData) {
+ // default implementation is empty, subclasses may override
+ }
+
/**
* Use this method to register all services of the device with
* {@link #registerService(BoschSHCService, Consumer, Collection, boolean)}.
*/
protected void initializeServices() throws BoschSHCException {
+ // default implementation is empty, subclasses may override
}
/**
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_CLIMATE_CONTROL;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_INTRUSION_DETECTION_SYSTEM;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_LIGHT_CONTROL_2;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_MOTION_DETECTOR;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SHC;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL_2;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SMART_BULB;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SMART_PLUG_COMPACT;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SMOKE_DETECTOR;
import org.openhab.binding.boschshc.internal.devices.camera.CameraHandler;
import org.openhab.binding.boschshc.internal.devices.climatecontrol.ClimateControlHandler;
import org.openhab.binding.boschshc.internal.devices.intrusion.IntrusionDetectionHandler;
+import org.openhab.binding.boschshc.internal.devices.lightcontrol.LightControl2Handler;
import org.openhab.binding.boschshc.internal.devices.lightcontrol.LightControlHandler;
import org.openhab.binding.boschshc.internal.devices.motiondetector.MotionDetectorHandler;
import org.openhab.binding.boschshc.internal.devices.plug.PlugHandler;
+import org.openhab.binding.boschshc.internal.devices.shuttercontrol.ShutterControl2Handler;
import org.openhab.binding.boschshc.internal.devices.shuttercontrol.ShutterControlHandler;
import org.openhab.binding.boschshc.internal.devices.smartbulb.SmartBulbHandler;
import org.openhab.binding.boschshc.internal.devices.smokedetector.SmokeDetector2Handler;
new ThingTypeHandlerMapping(THING_TYPE_WINDOW_CONTACT_2, WindowContact2Handler::new),
new ThingTypeHandlerMapping(THING_TYPE_MOTION_DETECTOR, MotionDetectorHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_SHUTTER_CONTROL, ShutterControlHandler::new),
+ new ThingTypeHandlerMapping(THING_TYPE_SHUTTER_CONTROL_2, ShutterControl2Handler::new),
new ThingTypeHandlerMapping(THING_TYPE_THERMOSTAT, ThermostatHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_CLIMATE_CONTROL, ClimateControlHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_WALL_THERMOSTAT, WallThermostatHandler::new),
thing -> new UniversalSwitchHandler(thing, timeZoneProvider)),
new ThingTypeHandlerMapping(THING_TYPE_UNIVERSAL_SWITCH_2,
thing -> new UniversalSwitch2Handler(thing, timeZoneProvider)),
- new ThingTypeHandlerMapping(THING_TYPE_SMOKE_DETECTOR_2, SmokeDetector2Handler::new));
+ new ThingTypeHandlerMapping(THING_TYPE_SMOKE_DETECTOR_2, SmokeDetector2Handler::new),
+ new ThingTypeHandlerMapping(THING_TYPE_LIGHT_CONTROL_2, LightControl2Handler::new));
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
* @return Bosch SHC URL for passed endpoint
*/
public String getBoschShcUrl(String endpoint) {
- return String.format("https://%s:8444/%s", this.ipAddress, endpoint);
+ String url = String.format("https://%s:8444/%s", this.ipAddress, endpoint);
+ return escapeURL(url);
+ }
+
+ /**
+ * Performs specific URL escaping required for certain Bosch SHC URLs.
+ * <p>
+ * In particular, hash characters in child device IDs must be escaped with <code>%23</code>.
+ * <p>
+ * Invalid example:
+ *
+ * <pre>
+ * https://host:port/devices/hdm:ZigBee:70ac08fffe5294ea#3/services/PowerSwitch/state
+ * </pre>
+ *
+ * Valid example:
+ *
+ * <pre>
+ * https://host:port/devices/hdm:ZigBee:70ac08fffe5294ea%233/services/PowerSwitch/state
+ * </pre>
+ *
+ * @param url the URL to be escaped
+ * @return the escaped URL
+ */
+ private String escapeURL(String url) {
+ return url.replace("#", "%23");
}
/**
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.openhab.binding.boschshc.internal.devices.BoschDeviceIdUtils;
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
*
* @param result Results from Long Polling
*/
- private void handleLongPollResult(LongPollResult result) {
+ void handleLongPollResult(LongPollResult result) {
for (BoschSHCServiceState serviceState : result.result) {
if (serviceState instanceof DeviceServiceData deviceServiceData) {
handleDeviceServiceData(deviceServiceData);
*/
private void forwardStateToHandlers(BoschSHCServiceState serviceData, JsonElement state, String updateDeviceId) {
boolean handled = false;
- final String serviceId;
- if (serviceData instanceof UserDefinedState userState) {
- serviceId = userState.getId();
- } else {
- serviceId = ((DeviceServiceData) serviceData).id;
- }
+ final String serviceId = getServiceId(serviceData);
Bridge bridge = this.getThing();
for (Thing childThing : bridge.getThings()) {
@Nullable
String deviceId = handler.getBoschID();
- handled = true;
- logger.debug("Registered device: {} - looking for {}", deviceId, updateDeviceId);
-
- if (deviceId != null && updateDeviceId.equals(deviceId)) {
- logger.debug("Found child: {} - calling processUpdate (id: {}) with {}", handler, serviceId, state);
- handler.processUpdate(serviceId, state);
+ if (deviceId == null) {
+ continue;
}
+
+ logger.trace("Checking device {}, looking for {}", deviceId, updateDeviceId);
+
+ // handled is a boolean latch that stays true once it becomes true
+ // note that no short-circuiting operators are used, meaning that the method
+ // calls will always be evaluated, even if the latch is already true
+ handled |= notifyHandler(handler, deviceId, updateDeviceId, serviceId, state);
+ handled |= notifyParentHandler(handler, deviceId, updateDeviceId, serviceId, state);
} else {
logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler);
}
}
}
+ /**
+ * 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.
*
/**
* Represents a single devices connected to the Bosch Smart Home Controller.
- *
- * Example from Json:
- *
+ * <p>
+ * Example JSON:
+ *
+ * <pre>
* {
- * "@type":"device",
- * "rootDeviceId":"64-da-a0-02-14-9b",
- * "id":"hdm:HomeMaticIP:3014F711A00004953859F31B",
- * "deviceServiceIds":["PowerMeter","PowerSwitch","PowerSwitchProgram","Routing"],
- * "manufacturer":"BOSCH",
- * "roomId":"hz_3",
- * "deviceModel":"PSM",
- * "serial":"3014F711A00004953859F31B",
- * "profile":"GENERIC",
- * "name":"Coffee Machine",
- * "status":"AVAILABLE",
- * "childDeviceIds":[]
+ * "@type": "device",
+ * "rootDeviceId": "64-da-a0-02-14-9b",
+ * "id": "hdm:HomeMaticIP:3014F711A00004953859F31B",
+ * "deviceServiceIds": ["PowerMeter","PowerSwitch","PowerSwitchProgram","Routing"],
+ * "manufacturer": "BOSCH",
+ * "roomId": "hz_3",
+ * "deviceModel": "PSM",
+ * "serial": "3014F711A00004953859F31B",
+ * "profile": "GENERIC",
+ * "name": "Coffee Machine",
+ * "status": "AVAILABLE",
+ * "childDeviceIds": []
* }
+ * </pre>
*
* @author Stefan Kästle - Initial contribution
*/
/**
* Response of the Controller for a Long Poll API call.
+ * <p>
+ * Example JSON:
+ *
+ * <pre>
+ * {
+ * "result": [{
+ * "path": "/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch",
+ * "@type": "DeviceServiceData",
+ * "id": "PowerSwitch",
+ * "state": {
+ * "@type": "powerSwitchState",
+ * "switchState": "ON"
+ * },
+ * "deviceId": "hdm:HomeMaticIP:3014F711A0001916D859A8A9"
+ * }],
+ * "jsonrpc": "2.0"
+ * }
+ * </pre>
*
* @author Stefan Kästle - Initial contribution
*/
public class LongPollResult {
- /**
- * {"result":[
- * ..{
- * ...."path":"/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch",
- * ...."@type":"DeviceServiceData",
- * ...."id":"PowerSwitch",
- * ...."state":{
- * ......"@type":"powerSwitchState",
- * ......"switchState":"ON"
- * ....},
- * ...."deviceId":"hdm:HomeMaticIP:3014F711A0001916D859A8A9"}
- * ],"jsonrpc":"2.0"}
- */
-
public ArrayList<BoschSHCServiceState> result;
+
public String jsonrpc;
}
--- /dev/null
+/**
+ * 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);
+ }
+}
package org.openhab.binding.boschshc.internal.devices.lightcontrol;
import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandler;
+import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerWithPowerMeter;
import org.openhab.core.thing.Thing;
/**
* @author Stefan Kästle - Initial contribution
*/
@NonNullByDefault
-public class LightControlHandler extends AbstractPowerSwitchHandler {
+public class LightControlHandler extends AbstractPowerSwitchHandlerWithPowerMeter {
public LightControlHandler(Thing thing) {
super(thing);
package org.openhab.binding.boschshc.internal.devices.plug;
import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandler;
+import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerWithPowerMeter;
import org.openhab.core.thing.Thing;
/**
* @author David Pace - Initial contribution
*/
@NonNullByDefault
-public class PlugHandler extends AbstractPowerSwitchHandler {
+public class PlugHandler extends AbstractPowerSwitchHandlerWithPowerMeter {
public PlugHandler(Thing thing) {
super(thing);
--- /dev/null
+/**
+ * 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);
+ }
+}
private final Logger logger = LoggerFactory.getLogger(ThingDiscoveryService.class);
+ /**
+ * Device model representing logical child devices of Light Control II
+ */
+ static final String DEVICE_MODEL_LIGHT_CONTROL_CHILD_DEVICE = "MICROMODULE_LIGHT_ATTACHED";
+
protected static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(
BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH, BoschSHCBindingConstants.THING_TYPE_TWINGUARD,
BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT, BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT_2,
new AbstractMap.SimpleEntry<>("TRV", BoschSHCBindingConstants.THING_TYPE_THERMOSTAT),
new AbstractMap.SimpleEntry<>("WRC2", BoschSHCBindingConstants.THING_TYPE_UNIVERSAL_SWITCH),
new AbstractMap.SimpleEntry<>("SWITCH2", BoschSHCBindingConstants.THING_TYPE_UNIVERSAL_SWITCH_2),
- new AbstractMap.SimpleEntry<>("SMOKE_DETECTOR2", BoschSHCBindingConstants.THING_TYPE_SMOKE_DETECTOR_2)
+ new AbstractMap.SimpleEntry<>("SMOKE_DETECTOR2", BoschSHCBindingConstants.THING_TYPE_SMOKE_DETECTOR_2),
+ new AbstractMap.SimpleEntry<>("MICROMODULE_SHUTTER", BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL_2),
+ new AbstractMap.SimpleEntry<>("MICROMODULE_AWNING", BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL_2),
+ new AbstractMap.SimpleEntry<>("MICROMODULE_LIGHT_CONTROL", BoschSHCBindingConstants.THING_TYPE_LIGHT_CONTROL_2)
// Future Extension: map deviceModel names to BoschSHC Thing Types when they are supported
// new AbstractMap.SimpleEntry<>("SMOKE_DETECTION_SYSTEM", BoschSHCBindingConstants.),
// new AbstractMap.SimpleEntry<>("PRESENCE_SIMULATION_SERVICE", BoschSHCBindingConstants.),
logger.trace("- got thingTypeID '{}' for deviceModel '{}'", thingTypeUID.getId(), device.deviceModel);
- ThingUID thingUID = new ThingUID(thingTypeUID, thingHandler.getThing().getUID(), device.id.replace(':', '_'));
+ ThingUID thingUID = new ThingUID(thingTypeUID, thingHandler.getThing().getUID(),
+ buildCompliantThingID(device.id));
logger.trace("- got thingUID '{}' for device: '{}'", thingUID, device);
DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID)
.withProperty("id", device.id).withLabel(getNiceName(device.name, roomName));
discoveryResult.withBridge(thingHandler.getThing().getUID());
+
if (!roomName.isEmpty()) {
discoveryResult.withProperty("Location", roomName);
}
thingUID, thingTypeUID, device.id, device.deviceModel);
}
+ /**
+ * Translates a Bosch device ID to an openHAB-compliant thing ID.
+ * <p>
+ * Characters that are not allowed in thing IDs are replaced by underscores.
+ *
+ * @param deviceId the Bosch device ID
+ * @return the translated openHAB-compliant thing ID
+ */
+ private String buildCompliantThingID(String deviceId) {
+ return deviceId.replace(':', '_').replace('#', '_');
+ }
+
private String getNiceName(String name, String roomName) {
if (!name.startsWith("-")) {
return name;
if (thingTypeId != null) {
return new ThingTypeUID(BoschSHCBindingConstants.BINDING_ID, thingTypeId.getId());
}
+
+ if (DEVICE_MODEL_LIGHT_CONTROL_CHILD_DEVICE.equals(device.deviceModel)) {
+ // Light Control II exposes a parent device and two child devices.
+ // We only add one thing for the parent device and the child devices are logically included.
+ // Therefore we do not need to add separate things for the child devices and need to suppress the
+ // log entry about the unknown device model.
+ return null;
+ }
+
logger.debug("Unknown deviceModel '{}'! Please create a support request issue for this unknown device model.",
device.deviceModel);
return null;
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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;
+}
@NonNullByDefault
public class PowerSwitchService extends BoschSHCService<PowerSwitchServiceState> {
+ public static final String POWER_SWITCH_SERVICE_NAME = "PowerSwitch";
+
public PowerSwitchService() {
- super("PowerSwitch", PowerSwitchServiceState.class);
+ super(POWER_SWITCH_SERVICE_NAME, PowerSwitchServiceState.class);
}
}
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
<config-description uri="thing-type:boschshc:bridge">
<parameter name="ipAddress" type="text" required="true">
<context>network-address</context>
<description>The system password of the Bosch Smart Home Controller necessary for pairing.</description>
</parameter>
</config-description>
+
<config-description uri="thing-type:boschshc:device">
<parameter name="id" type="text" required="true">
<label>Device ID</label>
<description>Unique ID of the device.</description>
</parameter>
</config-description>
+
<config-description uri="thing-type:boschshc:user-defined-state">
<parameter name="id" type="text" required="true">
<label>State ID</label>
<description>Unique ID of the state.</description>
</parameter>
</config-description>
+
</config-description:config-descriptions>
thing-type.boschshc.in-wall-switch.description = A simple light control.
thing-type.boschshc.intrusion-detection-system.label = Intrusion Detection System
thing-type.boschshc.intrusion-detection-system.description = Allows to retrieve and control the state of the intrusion detection alarm system.
+thing-type.boschshc.light-control-2.label = Light Control II
+thing-type.boschshc.light-control-2.description = Advanced light control with two switch circuits.
thing-type.boschshc.motion-detector.label = Motion Detector
thing-type.boschshc.motion-detector.description = Detects every movement through an intelligent combination of passive infra-red technology and an additional temperature sensor.
thing-type.boschshc.security-camera-360.label = Security Camera 360
thing-type.boschshc.security-camera-eyes.description = Outdoor security camera with motion detection and light.
thing-type.boschshc.shc.label = Smart Home Controller
thing-type.boschshc.shc.description = The Bosch Smart Home Bridge representing the Bosch Smart Home Controller.
+thing-type.boschshc.shutter-control-2.label = Shutter Control II
+thing-type.boschshc.shutter-control-2.description = Second generation shutter control.
thing-type.boschshc.shutter-control.label = Shutter Control
thing-type.boschshc.shutter-control.description = Control of your shutter to take any position you desire.
thing-type.boschshc.smart-bulb.label = Smart Bulb
channel-type.boschshc.camera-notification.state.option.OFF = Disabled
channel-type.boschshc.child-lock.label = Child Lock
channel-type.boschshc.child-lock.description = Enables or disables the child lock on the device.
+channel-type.boschshc.child-protection.label = Child Protection
+channel-type.boschshc.child-protection.description = Enables or disables the child protection on the device.
channel-type.boschshc.combined-rating.label = Combined Rating
channel-type.boschshc.combined-rating.description = Combined rating of the air quality.
channel-type.boschshc.combined-rating.state.option.GOOD = Good Quality
offline.conf-error.invalid-device-id = Device ID is invalid.
offline.conf-error.empty-state-id = No ID set.
offline.conf-error.invalid-state-id = ID is invalid.
+offline.conf-error.child-device-ids-not-obtainable = Could not obtain child device IDs.
</thing-type>
+ <thing-type id="shutter-control-2">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="shc"/>
+ </supported-bridge-type-refs>
+
+ <label>Shutter Control II</label>
+ <description>Second generation shutter control.</description>
+
+ <channels>
+ <channel id="level" typeId="level"/>
+ <channel id="signal-strength" typeId="system.signal-strength"/>
+ <channel id="child-protection" typeId="child-protection"/>
+ <channel id="power-consumption" typeId="power-consumption"/>
+ <channel id="energy-consumption" typeId="energy-consumption"/>
+ </channels>
+
+ <config-description-ref uri="thing-type:boschshc:device"/>
+
+ </thing-type>
+
+ <thing-type id="light-control-2">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="shc"/>
+ </supported-bridge-type-refs>
+
+ <label>Light Control II</label>
+ <description>Advanced light control with two switch circuits.</description>
+
+ <channels>
+ <channel id="signal-strength" typeId="system.signal-strength"/>
+ <channel id="power-consumption" typeId="power-consumption"/>
+ <channel id="energy-consumption" typeId="energy-consumption"/>
+ <channel id="power-switch-1" typeId="system.power"/>
+ <channel id="child-protection-1" typeId="child-protection"/>
+ <channel id="power-switch-2" typeId="system.power"/>
+ <channel id="child-protection-2" typeId="child-protection"/>
+ </channels>
+
+ <config-description-ref uri="thing-type:boschshc:device"/>
+
+ </thing-type>
+
<thing-type id="thermostat">
<supported-bridge-type-refs>
<bridge-type-ref id="shc"/>
<state readOnly="true"/>
</channel-type>
+ <channel-type id="child-protection">
+ <item-type>Switch</item-type>
+ <label>Child Protection</label>
+ <description>Enables or disables the child protection on the device.</description>
+ </channel-type>
+
</thing:thing-descriptions>
package org.openhab.binding.boschshc.internal.devices;
import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
import org.openhab.core.config.core.Configuration;
/**
public abstract class AbstractBoschSHCDeviceHandlerTest<T extends BoschSHCDeviceHandler>
extends AbstractBoschSHCHandlerTest<T> {
+ @Override
+ protected void configureDevice(Device device) {
+ super.configureDevice(device);
+
+ device.id = getDeviceID();
+ }
+
@Override
protected Configuration getConfiguration() {
Configuration configuration = super.getConfiguration();
*/
package org.openhab.binding.boschshc.internal.devices;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.binding.boschshc.internal.devices.bridge.BridgeHandler;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Bridge;
private @Mock @NonNullByDefault({}) ThingHandlerCallback callback;
+ private @NonNullByDefault({}) Device device;
+
protected AbstractBoschSHCHandlerTest() {
this.fixture = createFixture();
}
when(bridge.getHandler()).thenReturn(bridgeHandler);
lenient().when(thing.getConfiguration()).thenReturn(getConfiguration());
+ device = new Device();
+ configureDevice(device);
+ lenient().when(bridgeHandler.getDeviceInfo(anyString())).thenReturn(device);
+
fixture.initialize();
}
return callback;
}
+ protected Device getDevice() {
+ return device;
+ }
+
+ protected void configureDevice(Device device) {
+ // abstract implementation is empty, subclasses may override
+ }
+
@Test
public void testInitialize() {
ThingStatusInfo expectedStatusInfo = new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
*/
package org.openhab.binding.boschshc.internal.devices;
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
-import javax.measure.quantity.Energy;
-import javax.measure.quantity.Power;
-
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
-import org.openhab.binding.boschshc.internal.services.powermeter.dto.PowerMeterServiceState;
import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState;
import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState;
import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.QuantityType;
-import org.openhab.core.library.unit.Units;
import org.openhab.core.types.RefreshType;
import com.google.gson.JsonElement;
private @Captor @NonNullByDefault({}) ArgumentCaptor<PowerSwitchServiceState> serviceStateCaptor;
- private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Power>> powerCaptor;
-
- private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Energy>> energyCaptor;
-
@BeforeEach
@Override
public void beforeEach() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
powerSwitchServiceState.switchState = PowerSwitchState.ON;
lenient().when(bridgeHandler.getState(anyString(), eq("PowerSwitch"), same(PowerSwitchServiceState.class)))
.thenReturn(powerSwitchServiceState);
-
- PowerMeterServiceState powerMeterServiceState = new PowerMeterServiceState();
- powerMeterServiceState.powerConsumption = 12.34d;
- powerMeterServiceState.energyConsumption = 56.78d;
- lenient().when(bridgeHandler.getState(anyString(), eq("PowerMeter"), same(PowerMeterServiceState.class)))
- .thenReturn(powerMeterServiceState);
}
@Test
verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.OFF);
}
- @Test
- public void testUpdateChannelPowerMeterServiceState() {
- JsonElement jsonObject = JsonParser.parseString("""
- {
- "@type": "powerMeterState",
- "powerConsumption": "23",
- "energyConsumption": 42
- }\
- """);
- getFixture().processUpdate("PowerMeter", jsonObject);
-
- verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION)),
- powerCaptor.capture());
- QuantityType<Power> powerValue = powerCaptor.getValue();
- assertEquals(23, powerValue.intValue());
-
- verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION)),
- energyCaptor.capture());
- QuantityType<Energy> energyValue = energyCaptor.getValue();
- assertEquals(42, energyValue.intValue());
- }
-
@Test
public void testHandleCommandRefreshPowerSwitchChannel() {
getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), RefreshType.REFRESH);
verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.ON);
}
-
- @Test
- public void testHandleCommandRefreshPowerConsumptionChannel() {
- getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION),
- RefreshType.REFRESH);
- verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION),
- new QuantityType<>(12.34d, Units.WATT));
- }
-
- @Test
- public void testHandleCommandRefreshEnergyConsumptionChannel() {
- getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION),
- RefreshType.REFRESH);
- verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION),
- new QuantityType<>(56.78d, Units.WATT_HOUR));
- }
}
--- /dev/null
+/**
+ * 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));
+ }
+}
--- /dev/null
+/**
+ * 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"));
+ }
+}
httpClient.getServiceStateUrl("testService", "testDevice", UserStateServiceState.class));
}
+ @Test
+ void getServiceStateUrlForChildDevice() {
+ assertEquals(
+ "https://127.0.0.1:8444/smarthome/devices/hdm:ZigBee:70ac08fffe5294ea%233/services/PowerSwitch/state",
+ httpClient.getServiceStateUrl("PowerSwitch", "hdm:ZigBee:70ac08fffe5294ea#3"));
+ }
+
@Test
void isAccessPossible() throws InterruptedException {
assertFalse(httpClient.isAccessPossible());
*/
package org.openhab.binding.boschshc.internal.devices.bridge;
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.contains;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
+import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceTest;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Faults;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedStateTest;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+
/**
* Unit tests for the {@link BridgeHandler}.
*
private @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
+ /**
+ * A mocked bridge instance
+ */
+ private @NonNullByDefault({}) Bridge thing;
+
@BeforeAll
static void beforeAll() throws IOException {
Path mavenTargetFolder = Paths.get("target");
properties.put("password", "test");
bridgeConfiguration.setProperties(properties);
- Thing thing = mock(Bridge.class);
+ thing = mock(Bridge.class);
when(thing.getConfiguration()).thenReturn(bridgeConfiguration);
// this calls initialize() as well
fixture.thingUpdated(thing);
void afterEach() throws Exception {
fixture.dispose();
}
+
+ @Test
+ void handleLongPollResultNoDeviceId() {
+ List<Thing> things = new ArrayList<Thing>();
+ when(thing.getThings()).thenReturn(things);
+
+ Thing thing = mock(Thing.class);
+ things.add(thing);
+
+ BoschSHCHandler thingHandler = mock(BoschSHCHandler.class);
+ when(thing.getHandler()).thenReturn(thingHandler);
+
+ String json = """
+ {
+ "result": [{
+ "path": "/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch",
+ "@type": "DeviceServiceData",
+ "id": "PowerSwitch",
+ "state": {
+ "@type": "powerSwitchState",
+ "switchState": "ON"
+ },
+ "deviceId": "hdm:HomeMaticIP:3014F711A0001916D859A8A9"
+ }],
+ "jsonrpc": "2.0"
+ }
+ """;
+ LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, LongPollResult.class);
+ assertNotNull(longPollResult);
+
+ fixture.handleLongPollResult(longPollResult);
+
+ verify(thingHandler).getBoschID();
+ verifyNoMoreInteractions(thingHandler);
+ }
+
+ @Test
+ void handleLongPollResult() {
+ List<Thing> things = new ArrayList<Thing>();
+ when(thing.getThings()).thenReturn(things);
+
+ Thing thing = mock(Thing.class);
+ things.add(thing);
+
+ BoschSHCHandler thingHandler = mock(BoschSHCHandler.class);
+ when(thing.getHandler()).thenReturn(thingHandler);
+
+ when(thingHandler.getBoschID()).thenReturn("hdm:HomeMaticIP:3014F711A0001916D859A8A9");
+
+ String json = """
+ {
+ "result": [{
+ "path": "/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch",
+ "@type": "DeviceServiceData",
+ "id": "PowerSwitch",
+ "state": {
+ "@type": "powerSwitchState",
+ "switchState": "ON"
+ },
+ "deviceId": "hdm:HomeMaticIP:3014F711A0001916D859A8A9"
+ }],
+ "jsonrpc": "2.0"
+ }
+ """;
+ LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, LongPollResult.class);
+ assertNotNull(longPollResult);
+
+ fixture.handleLongPollResult(longPollResult);
+
+ verify(thingHandler).getBoschID();
+
+ JsonElement expectedState = JsonParser.parseString("""
+ {
+ "@type": "powerSwitchState",
+ "switchState": "ON"
+ }
+ """);
+
+ verify(thingHandler).processUpdate("PowerSwitch", expectedState);
+ }
+
+ @Test
+ void handleLongPollResultHandleChildUpdate() {
+ List<Thing> things = new ArrayList<Thing>();
+ when(thing.getThings()).thenReturn(things);
+
+ Thing thing = mock(Thing.class);
+ things.add(thing);
+
+ BoschSHCHandler thingHandler = mock(BoschSHCHandler.class);
+ when(thing.getHandler()).thenReturn(thingHandler);
+
+ when(thingHandler.getBoschID()).thenReturn("hdm:ZigBee:70ac08fffefead2d");
+
+ String json = """
+ {
+ "result": [{
+ "path": "/devices/hdm:ZigBee:70ac08fffefead2d#3/services/PowerSwitch",
+ "@type": "DeviceServiceData",
+ "id": "PowerSwitch",
+ "state": {
+ "@type": "powerSwitchState",
+ "switchState": "ON"
+ },
+ "deviceId": "hdm:ZigBee:70ac08fffefead2d#3"
+ }],
+ "jsonrpc": "2.0"
+ }
+ """;
+ LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, LongPollResult.class);
+ assertNotNull(longPollResult);
+
+ fixture.handleLongPollResult(longPollResult);
+
+ verify(thingHandler).getBoschID();
+
+ JsonElement expectedState = JsonParser.parseString("""
+ {
+ "@type": "powerSwitchState",
+ "switchState": "ON"
+ }
+ """);
+
+ verify(thingHandler).processChildUpdate("hdm:ZigBee:70ac08fffefead2d#3", "PowerSwitch", expectedState);
+ }
}
--- /dev/null
+/**
+ * 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());
+ }
+}
package org.openhab.binding.boschshc.internal.devices.lightcontrol;
import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerTest;
+import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerWithPowerMeterTest;
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.core.thing.ThingTypeUID;
*
*/
@NonNullByDefault
-public class LightControlHandlerTest extends AbstractPowerSwitchHandlerTest<LightControlHandler> {
+class LightControlHandlerTest extends AbstractPowerSwitchHandlerWithPowerMeterTest<LightControlHandler> {
@Override
protected ThingTypeUID getThingTypeUID() {
package org.openhab.binding.boschshc.internal.devices.plug;
import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerTest;
+import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerWithPowerMeterTest;
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.core.thing.ThingTypeUID;
*
*/
@NonNullByDefault
-public class PlugHandlerTest extends AbstractPowerSwitchHandlerTest<PlugHandler> {
+class PlugHandlerTest extends AbstractPowerSwitchHandlerWithPowerMeterTest<PlugHandler> {
@Override
protected PlugHandler createFixture() {
--- /dev/null
+/**
+ * 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);
+ }
+}
package org.openhab.binding.boschshc.internal.discovery;
import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.UUID;
import org.openhab.core.thing.ThingUID;
/**
- * ThingDiscoveryService Tester.
+ * Unit tests for {@link ThingDiscoveryService}.
*
* @author Gerd Zanker - Initial contribution
*/
// two calls for the two devices expected
verify(discoveryListener, times(2)).thingDiscovered(any(), any());
}
+
+ @Test
+ void getThingTypeUIDLightControl2ChildDevice() {
+ Device device = new Device();
+ device.deviceModel = ThingDiscoveryService.DEVICE_MODEL_LIGHT_CONTROL_CHILD_DEVICE;
+
+ assertThat(fixture.getThingTypeUID(device), is(nullValue()));
+ }
}