diff --git a/bundles/org.openhab.binding.boschshc/README.md b/bundles/org.openhab.binding.boschshc/README.md
index 27b2666987..0b1c4bb73e 100644
--- a/bundles/org.openhab.binding.boschshc/README.md
+++ b/bundles/org.openhab.binding.boschshc/README.md
@@ -236,14 +236,15 @@ First generation radiator thermostat.
**Thing Type ID**: `thermostat`
-| Channel ID | Item Type | Writable | Description |
-| --------------------- | -------------------- | :------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| temperature | Number:Temperature | no | Current measured temperature. |
-| valve-tappet-position | Number:Dimensionless | no | Current open ratio of valve tappet (0 to 100). |
-| child-lock | Switch | yes | Indicates if child lock is active. |
-| silent-mode | Switch | yes | Enables or disables silent mode on thermostats. When enabled, the battery usage is higher. |
-| battery-level | Number | no | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. |
-| low-battery | Switch | no | Indicates whether the battery is low (`ON`) or OK (`OFF`). |
+| Channel ID | Item Type | Writable | Description |
+| --------------------- | -------------------- | :------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| temperature | Number:Temperature | no | Current measured temperature. |
+| temperature-offset | Number:Temperature | yes | Temperature offset to be added or subtracted from the measured temperature. Value range for Celsius and Kelvin quantities: minimum -5°C / -5 K, maximum +5°C / +5 K. Value range for Fahrenheit quantities: minimum -9°F, maximum +9°F. Step size: 0.1°C. Non-Celsius temperature offsets will be converted to Celsius deltas automatically and rounded to the first decimal digit. |
+| valve-tappet-position | Number:Dimensionless | no | Current open ratio of valve tappet (0 to 100). |
+| child-lock | Switch | yes | Indicates if child lock is active. |
+| silent-mode | Switch | yes | Enables or disables silent mode on thermostats. When enabled, the battery usage is higher. |
+| battery-level | Number | no | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. |
+| low-battery | Switch | no | Indicates whether the battery is low (`ON`) or OK (`OFF`). |
### Radiator Thermostat II
@@ -251,15 +252,16 @@ Second generation radiator thermostat.
**Thing Type ID**: `thermostat-2`
-| Channel ID | Item Type | Writable | Description |
-| --------------------- | -------------------- | :------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| temperature | Number:Temperature | no | Current measured temperature. |
-| valve-tappet-position | Number:Dimensionless | no | Current open ratio of valve tappet (0 to 100). |
-| child-lock | Switch | yes | Indicates if child lock is active. |
-| battery-level | Number | no | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. |
-| low-battery | Switch | no | Indicates whether the battery is low (`ON`) or OK (`OFF`). |
-| display-direction | Switch | yes | Configures the display direction. `OFF` corresponds to `NORMAL` and `ON` corresponds to `REVERSED`. |
-| displayed-temperature | Switch | yes | Configures whether the measured temperature or the setpoint temperature is displayed. `OFF` corresponds to `MEASURED` and `ON` corresponds to `SETPOINT`. |
+| Channel ID | Item Type | Writable | Description |
+| --------------------- | -------------------- | :------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| temperature | Number:Temperature | no | Current measured temperature. |
+| temperature-offset | Number:Temperature | yes | Temperature offset to be added or subtracted from the measured temperature. Value range for Celsius and Kelvin quantities: minimum -5°C / -5 K, maximum +5°C / +5 K. Value range for Fahrenheit quantities: minimum -9°F, maximum +9°F. Step size: 0.1°C. Non-Celsius temperature offsets will be converted to Celsius deltas automatically and rounded to the first decimal digit. |
+| valve-tappet-position | Number:Dimensionless | no | Current open ratio of valve tappet (0 to 100). |
+| child-lock | Switch | yes | Indicates if child lock is active. |
+| battery-level | Number | no | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. |
+| low-battery | Switch | no | Indicates whether the battery is low (`ON`) or OK (`OFF`). |
+| display-direction | Switch | yes | Configures the display direction. `OFF` corresponds to `NORMAL` and `ON` corresponds to `REVERSED`. |
+| displayed-temperature | Switch | yes | Configures whether the measured temperature or the setpoint temperature is displayed. `OFF` corresponds to `MEASURED` and `ON` corresponds to `SETPOINT`. |
### Radiator Thermostat II [+M]
@@ -267,15 +269,16 @@ Second generation radiator thermostat with Matter support.
**Thing Type ID**: `thermostat-2-matter`
-| Channel ID | Item Type | Writable | Description |
-| --------------------- | -------------------- | :------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| temperature | Number:Temperature | no | Current measured temperature. |
-| valve-tappet-position | Number:Dimensionless | no | Current open ratio of valve tappet (0 to 100). |
-| child-lock | Switch | yes | Indicates if child lock is active. |
-| battery-level | Number | no | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. |
-| low-battery | Switch | no | Indicates whether the battery is low (`ON`) or OK (`OFF`). |
-| display-direction | Switch | yes | Configures the display direction. `OFF` corresponds to `NORMAL` and `ON` corresponds to `REVERSED`. |
-| displayed-temperature | Switch | yes | Configures whether the measured temperature or the setpoint temperature is displayed. `OFF` corresponds to `MEASURED` and `ON` corresponds to `SETPOINT`. |
+| Channel ID | Item Type | Writable | Description |
+| --------------------- | -------------------- | :------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| temperature | Number:Temperature | no | Current measured temperature. |
+| temperature-offset | Number:Temperature | yes | Temperature offset to be added or subtracted from the measured temperature. Value range for Celsius and Kelvin quantities: minimum -5°C / -5 K, maximum +5°C / +5 K. Value range for Fahrenheit quantities: minimum -9°F, maximum +9°F. Step size: 0.1°C. Non-Celsius temperature offsets will be converted to Celsius deltas automatically and rounded to the first decimal digit. |
+| valve-tappet-position | Number:Dimensionless | no | Current open ratio of valve tappet (0 to 100). |
+| child-lock | Switch | yes | Indicates if child lock is active. |
+| battery-level | Number | no | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. |
+| low-battery | Switch | no | Indicates whether the battery is low (`ON`) or OK (`OFF`). |
+| display-direction | Switch | yes | Configures the display direction. `OFF` corresponds to `NORMAL` and `ON` corresponds to `REVERSED`. |
+| displayed-temperature | Switch | yes | Configures whether the measured temperature or the setpoint temperature is displayed. `OFF` corresponds to `MEASURED` and `ON` corresponds to `SETPOINT`. |
### Climate Control
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtension.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtension.java
index 2326e50b69..4a2c62cbcc 100644
--- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtension.java
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtension.java
@@ -86,8 +86,8 @@ public class BoschShcCommandExtension extends AbstractConsoleCommandExtension im
"displayedtemperatureconfiguration", "hsbcoloractuator", "humiditylevel", "illuminance",
"impulseswitch", "intrusion", "keypad", "latestmotion", "multilevelswitch", "powermeter", "powerswitch",
"presence", "privacymode", "roomclimatecontrol", "shuttercontact", "shuttercontrol", "silentmode",
- "smokedetectorcheck", "temperaturelevel", "userstate", "valvetappet", "vibrationsensor",
- "waterleakagesensor", "waterleakagesensorcheck", "waterleakagesensortilt");
+ "smokedetectorcheck", "temperaturelevel", "temperatureoffset", "userstate", "valvetappet",
+ "vibrationsensor", "waterleakagesensor", "waterleakagesensorcheck", "waterleakagesensortilt");
}
@Override
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java
index b37387d32e..b46ea516f6 100644
--- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java
@@ -79,6 +79,7 @@ public class BoschSHCBindingConstants {
public static final String CHANNEL_TRIGGER_SCENARIO = "trigger-scenario";
public static final String CHANNEL_POWER_SWITCH = "power-switch";
public static final String CHANNEL_TEMPERATURE = "temperature";
+ public static final String CHANNEL_TEMPERATURE_OFFSET = "temperature-offset";
public static final String CHANNEL_TEMPERATURE_RATING = "temperature-rating";
public static final String CHANNEL_HUMIDITY = "humidity";
public static final String CHANNEL_HUMIDITY_RATING = "humidity-rating";
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java
index d83be22c25..157ac35104 100644
--- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java
@@ -402,7 +402,7 @@ public abstract class BoschSHCHandler extends BaseThingHandler {
TService service, TState state) {
try {
service.setState(state);
- } catch (TimeoutException | ExecutionException e) {
+ } catch (TimeoutException | ExecutionException | BoschSHCException e) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String.format(
"Error while trying to update state for service %s: %s", service.getServiceName(), e.getMessage()));
} catch (InterruptedException e) {
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java
index 31e9ec5688..97cb2d3aa1 100644
--- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java
@@ -943,8 +943,8 @@ public class BridgeHandler extends BaseBridgeHandler {
* @throws ExecutionException
* @throws TimeoutException
*/
- public @Nullable Response putState(String deviceId, String serviceName, T state)
- throws InterruptedException, TimeoutException, ExecutionException {
+ public @Nullable ContentResponse putState(String deviceId, String serviceName,
+ T state) throws InterruptedException, TimeoutException, ExecutionException {
return sendState(deviceId, serviceName, state, PUT);
}
@@ -963,13 +963,13 @@ public class BridgeHandler extends BaseBridgeHandler {
* @throws TimeoutException
* @throws ExecutionException
*/
- public @Nullable Response postState(String deviceId, String serviceName, T state)
- throws InterruptedException, TimeoutException, ExecutionException {
+ public @Nullable ContentResponse postState(String deviceId, String serviceName,
+ T state) throws InterruptedException, TimeoutException, ExecutionException {
return sendState(deviceId, serviceName, state, POST);
}
- private @Nullable Response sendState(String deviceId, String serviceName, T state,
- HttpMethod method) throws InterruptedException, TimeoutException, ExecutionException {
+ private @Nullable ContentResponse sendState(String deviceId, String serviceName,
+ T state, HttpMethod method) throws InterruptedException, TimeoutException, ExecutionException {
@Nullable
BoschHttpClient localHttpClient = this.httpClient;
if (localHttpClient == null) {
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/AbstractThermostatHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/AbstractThermostatHandler.java
index 0295735e5c..b82b5e3efc 100644
--- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/AbstractThermostatHandler.java
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/AbstractThermostatHandler.java
@@ -23,6 +23,8 @@ import org.openhab.binding.boschshc.internal.services.childlock.ChildLockService
import org.openhab.binding.boschshc.internal.services.childlock.dto.ChildLockServiceState;
import org.openhab.binding.boschshc.internal.services.temperaturelevel.TemperatureLevelService;
import org.openhab.binding.boschshc.internal.services.temperaturelevel.dto.TemperatureLevelServiceState;
+import org.openhab.binding.boschshc.internal.services.temperatureoffset.TemperatureOffsetService;
+import org.openhab.binding.boschshc.internal.services.temperatureoffset.dto.TemperatureOffsetServiceState;
import org.openhab.binding.boschshc.internal.services.valvetappet.ValveTappetService;
import org.openhab.binding.boschshc.internal.services.valvetappet.dto.ValveTappetServiceState;
import org.openhab.core.thing.ChannelUID;
@@ -46,10 +48,12 @@ import org.openhab.core.types.Command;
public abstract class AbstractThermostatHandler extends AbstractBatteryPoweredDeviceHandler {
private ChildLockService childLockService;
+ private TemperatureOffsetService temperatureOffsetService;
protected AbstractThermostatHandler(Thing thing) {
super(thing);
this.childLockService = new ChildLockService();
+ this.temperatureOffsetService = new TemperatureOffsetService();
}
@Override
@@ -60,6 +64,7 @@ public abstract class AbstractThermostatHandler extends AbstractBatteryPoweredDe
this.createService(ValveTappetService::new, this::updateChannels, List.of(CHANNEL_VALVE_TAPPET_POSITION));
this.registerService(this.childLockService, this::updateChannels, List.of(CHANNEL_CHILD_LOCK));
+ this.registerService(this.temperatureOffsetService, this::updateChannels, List.of(CHANNEL_TEMPERATURE_OFFSET));
}
@Override
@@ -68,6 +73,8 @@ public abstract class AbstractThermostatHandler extends AbstractBatteryPoweredDe
if (CHANNEL_CHILD_LOCK.equals(channelUID.getId())) {
this.handleServiceCommand(this.childLockService, command);
+ } else if (CHANNEL_TEMPERATURE_OFFSET.equals(channelUID.getId())) {
+ this.handleServiceCommand(this.temperatureOffsetService, command);
}
}
@@ -75,17 +82,30 @@ public abstract class AbstractThermostatHandler extends AbstractBatteryPoweredDe
* Updates the channels which are linked to the {@link TemperatureLevelService}
* of the device.
*
- * @param state Current state of {@link TemperatureLevelService}.
+ * @param state current {@link TemperatureLevelServiceState} received from the
+ * Smart Home Controller
*/
private void updateChannels(TemperatureLevelServiceState state) {
super.updateState(CHANNEL_TEMPERATURE, state.getTemperatureState());
}
+ /**
+ * Updates the channels which are linked to the {@link TemperatureOffsetService}
+ * of the device.
+ *
+ * @param state current {@link TemperatureOffsetServiceState} received from the Smart
+ * Home Controller
+ */
+ private void updateChannels(TemperatureOffsetServiceState state) {
+ super.updateState(CHANNEL_TEMPERATURE_OFFSET, state.getOffsetState());
+ }
+
/**
* Updates the channels which are linked to the {@link ValveTappetService} of
* the device.
*
- * @param state Current state of {@link ValveTappetService}.
+ * @param state current {@link ValveTappetServiceState} received from the Smart
+ * Home Controller
*/
private void updateChannels(ValveTappetServiceState state) {
super.updateState(CHANNEL_VALVE_TAPPET_POSITION, state.getPositionState());
@@ -95,7 +115,8 @@ public abstract class AbstractThermostatHandler extends AbstractBatteryPoweredDe
* Updates the channels which are linked to the {@link ChildLockService} of the
* device.
*
- * @param state Current state of {@link ChildLockService}.
+ * @param state current {@link ChildLockServiceState} received from the Smart
+ * Home Controller
*/
private void updateChannels(ChildLockServiceState state) {
super.updateState(CHANNEL_CHILD_LOCK, state.getActiveState());
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/BoschSHCService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/BoschSHCService.java
index 650d72d281..1135e8e790 100644
--- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/BoschSHCService.java
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/BoschSHCService.java
@@ -18,6 +18,7 @@ import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.api.ContentResponse;
import org.openhab.binding.boschshc.internal.devices.bridge.BridgeHandler;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
@@ -49,7 +50,7 @@ import com.google.gson.JsonElement;
@NonNullByDefault
public abstract class BoschSHCService extends AbstractBoschSHCService {
- protected final Logger logger = LoggerFactory.getLogger(BoschSHCService.class);
+ private final Logger logger = LoggerFactory.getLogger(BoschSHCService.class);
/**
* Class of service state
@@ -141,8 +142,10 @@ public abstract class BoschSHCService exten
* @throws InterruptedException
* @throws ExecutionException
* @throws TimeoutException
+ * @throws BoschSHCException
*/
- public void setState(TState state) throws InterruptedException, TimeoutException, ExecutionException {
+ public void setState(TState state)
+ throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
String deviceId = getDeviceId();
if (deviceId == null) {
return;
@@ -151,7 +154,18 @@ public abstract class BoschSHCService exten
if (bridgeHandler == null) {
return;
}
- bridgeHandler.putState(deviceId, getServiceName(), state);
+
+ @Nullable
+ ContentResponse response = bridgeHandler.putState(deviceId, getServiceName(), state);
+ if (response != null) {
+ int status = response.getStatus();
+ if (status >= 400) {
+ String responseBody = response.getContentAsString();
+ throw new BoschSHCException(String.format(
+ "Error while updating state for service %s. SHC returned status code %d with response %s",
+ getServiceName(), status, responseBody));
+ }
+ }
}
/**
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperatureoffset/TemperatureOffsetService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperatureoffset/TemperatureOffsetService.java
new file mode 100644
index 0000000000..c4f13e8f5b
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperatureoffset/TemperatureOffsetService.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2010-2026 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.temperatureoffset;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+import javax.measure.quantity.Temperature;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.services.BoschSHCService;
+import org.openhab.binding.boschshc.internal.services.temperatureoffset.dto.TemperatureOffsetServiceState;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.types.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Service to configure temperature offsets for thermostats.
+ *
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class TemperatureOffsetService extends BoschSHCService {
+
+ private final Logger logger = LoggerFactory.getLogger(TemperatureOffsetService.class);
+
+ public static final String TEMPERATURE_OFFSET_SERVICE_NAME = "TemperatureOffset";
+
+ public static final BigDecimal MINIMUM_OFFSET = BigDecimal.valueOf(-5);
+ public static final BigDecimal MAXIMUM_OFFSET = BigDecimal.valueOf(5);
+
+ public TemperatureOffsetService() {
+ super(TEMPERATURE_OFFSET_SERVICE_NAME, TemperatureOffsetServiceState.class);
+ }
+
+ @Override
+ public TemperatureOffsetServiceState handleCommand(Command command) throws BoschSHCException {
+ if (command instanceof DecimalType numberCommand) {
+ return createNewTemperatureOffsetState(numberCommand.toBigDecimal());
+ } else if (command instanceof QuantityType> quantityCommand) {
+ @Nullable
+ QuantityType relativeValue = getRelativeTemperatureValue(quantityCommand);
+ if (relativeValue != null) {
+ return createNewTemperatureOffsetState(relativeValue.toBigDecimal());
+ }
+ }
+ return super.handleCommand(command);
+ }
+
+ private @Nullable QuantityType getRelativeTemperatureValue(QuantityType> quantityCommand) {
+ // check if the given quantity has a temperature unit
+ if (!quantityCommand.getUnit().isCompatible(SIUnits.CELSIUS)) {
+ logger.warn("Received a command that has no temperature quantity: {}", quantityCommand);
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ QuantityType temperatureCommand = (QuantityType) quantityCommand;
+ return temperatureCommand.toUnitRelative(SIUnits.CELSIUS);
+ }
+
+ private TemperatureOffsetServiceState createNewTemperatureOffsetState(BigDecimal offset) {
+ TemperatureOffsetServiceState state = new TemperatureOffsetServiceState();
+ double sanitizedOffset = sanitizeOffsetValue(offset);
+ logger.debug("Temperature offset value {} was sanitized to {}.", offset, sanitizedOffset);
+ state.offset = sanitizedOffset;
+ return state;
+ }
+
+ static double sanitizeOffsetValue(BigDecimal bigDecimal) {
+ if (bigDecimal.compareTo(MINIMUM_OFFSET) < 0) {
+ return MINIMUM_OFFSET.doubleValue();
+ } else if (bigDecimal.compareTo(MAXIMUM_OFFSET) > 0) {
+ return MAXIMUM_OFFSET.doubleValue();
+ }
+
+ // accept at most one decimal digit
+ if (bigDecimal.scale() > 1) {
+ // round the value in case more than one decimal digits are provided
+ return bigDecimal.setScale(1, RoundingMode.HALF_UP).doubleValue();
+ }
+
+ return bigDecimal.doubleValue();
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperatureoffset/dto/TemperatureOffsetServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperatureoffset/dto/TemperatureOffsetServiceState.java
new file mode 100644
index 0000000000..efb91dddba
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/temperatureoffset/dto/TemperatureOffsetServiceState.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2010-2026 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.temperatureoffset.dto;
+
+import javax.measure.quantity.Temperature;
+
+import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.SIUnits;
+
+/**
+ * DTO for temperature offset states.
+ *
+ * @author David Pace - Initial contribution
+ *
+ */
+public class TemperatureOffsetServiceState extends BoschSHCServiceState {
+
+ public TemperatureOffsetServiceState() {
+ super("temperatureOffsetState");
+ }
+
+ public double offset;
+
+ // Note: the following members are Double objects intentionally in order to make them nullable.
+ // Those members must NOT be serialized for outgoing status updates, otherwise they will be rejected
+ // by the Smart Home Controller, but they are needed for incoming service state updates
+ public Double stepSize;
+ public Double minOffset;
+ public Double maxOffset;
+
+ public QuantityType getOffsetState() {
+ return new QuantityType<>(offset, SIUnits.CELSIUS);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties
index 69bf17df59..2a938ea1ed 100644
--- a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties
+++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties
@@ -214,6 +214,8 @@ channel-type.boschshc.smoke-check.state.option.SMOKE_TEST_OK = Test successful
channel-type.boschshc.smoke-check.state.option.SMOKE_TEST_FAILED = Test failed
channel-type.boschshc.system-availability.label = System Availability
channel-type.boschshc.system-availability.description = Indicates whether the intrusion detection system is available.
+channel-type.boschshc.temperature-offset.label = Temperature Offset
+channel-type.boschshc.temperature-offset.description = Temperature offset to be added or subtracted from the measured temperature.
channel-type.boschshc.temperature-rating.label = Temperature Rating
channel-type.boschshc.temperature-rating.description = Rating of the currently measured temperature.
channel-type.boschshc.temperature-rating.state.option.GOOD = Good Temperature
diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml
index 31459d03e8..176de61633 100644
--- a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml
+++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml
@@ -275,6 +275,7 @@
+
@@ -282,6 +283,10 @@
+
+ 1
+
+
@@ -297,6 +302,7 @@
+
@@ -305,6 +311,10 @@
+
+ 1
+
+
@@ -320,6 +330,7 @@
+
@@ -328,6 +339,10 @@
+
+ 1
+
+
@@ -730,6 +745,16 @@
+
+ Number:Temperature
+
+ Temperature offset to be added or subtracted from the measured temperature.
+
+ Temperature
+
+
+
+
String
diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/update/binding.xml b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/update/binding.xml
index 5d440641c3..48de5bce6d 100644
--- a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/update/binding.xml
+++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/update/binding.xml
@@ -38,4 +38,28 @@
+
+
+
+ boschshc:temperature-offset
+
+
+
+
+
+
+
+ boschshc:temperature-offset
+
+
+
+
+
+
+
+ boschshc:temperature-offset
+
+
+
+
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/thermostat/AbstractThermostatHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/thermostat/AbstractThermostatHandlerTest.java
index dd9ddc452e..6541415fab 100644
--- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/thermostat/AbstractThermostatHandlerTest.java
+++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/thermostat/AbstractThermostatHandlerTest.java
@@ -12,9 +12,9 @@
*/
package org.openhab.binding.boschshc.internal.devices.thermostat;
-import static org.junit.jupiter.api.Assertions.assertSame;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.verify;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
@@ -27,10 +27,14 @@ import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDevic
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.binding.boschshc.internal.services.childlock.dto.ChildLockServiceState;
import org.openhab.binding.boschshc.internal.services.childlock.dto.ChildLockState;
+import org.openhab.binding.boschshc.internal.services.temperatureoffset.TemperatureOffsetService;
+import org.openhab.binding.boschshc.internal.services.temperatureoffset.dto.TemperatureOffsetServiceState;
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.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
@@ -51,6 +55,7 @@ public abstract class AbstractThermostatHandlerTest {
private @Captor @NonNullByDefault({}) ArgumentCaptor childLockServiceStateCaptor;
+ private @Captor @NonNullByDefault({}) ArgumentCaptor temperatureOffsetCaptor;
@Test
void testHandleCommandChildLockService() throws InterruptedException, TimeoutException, ExecutionException {
@@ -113,4 +118,130 @@ public abstract class AbstractThermostatHandlerTest(1.3d, SIUnits.CELSIUS));
+ }
+
+ @Test
+ void testHandleCommandTemperatureOffsetServiceQuantityTypeCelsius()
+ throws InterruptedException, TimeoutException, ExecutionException {
+ getFixture().handleCommand(
+ new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_TEMPERATURE_OFFSET),
+ new QuantityType<>(-1.3d, SIUnits.CELSIUS));
+ verify(getBridgeHandler()).putState(eq(getDeviceID()),
+ eq(TemperatureOffsetService.TEMPERATURE_OFFSET_SERVICE_NAME), temperatureOffsetCaptor.capture());
+ TemperatureOffsetServiceState state = temperatureOffsetCaptor.getValue();
+ assertNull(state.minOffset);
+ assertNull(state.maxOffset);
+ assertNull(state.stepSize);
+ assertEquals(-1.3d, state.offset, 0.001d);
+ }
+
+ @Test
+ void testHandleCommandTemperatureOffsetServiceQuantityTypeCelsiusExceedMaxOffset()
+ throws InterruptedException, TimeoutException, ExecutionException {
+ getFixture().handleCommand(
+ new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_TEMPERATURE_OFFSET),
+ new QuantityType<>(6d, SIUnits.CELSIUS));
+ verify(getBridgeHandler()).putState(eq(getDeviceID()),
+ eq(TemperatureOffsetService.TEMPERATURE_OFFSET_SERVICE_NAME), temperatureOffsetCaptor.capture());
+ TemperatureOffsetServiceState state = temperatureOffsetCaptor.getValue();
+ assertNull(state.minOffset);
+ assertNull(state.maxOffset);
+ assertNull(state.stepSize);
+ assertEquals(5d, state.offset, 0.001d);
+ }
+
+ @Test
+ void testHandleCommandTemperatureOffsetServiceQuantityTypeFahrenheit()
+ throws InterruptedException, TimeoutException, ExecutionException {
+ getFixture().handleCommand(
+ new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_TEMPERATURE_OFFSET),
+ new QuantityType<>(-1.3d, ImperialUnits.FAHRENHEIT));
+ verify(getBridgeHandler()).putState(eq(getDeviceID()),
+ eq(TemperatureOffsetService.TEMPERATURE_OFFSET_SERVICE_NAME), temperatureOffsetCaptor.capture());
+ TemperatureOffsetServiceState state = temperatureOffsetCaptor.getValue();
+ assertNull(state.minOffset);
+ assertNull(state.maxOffset);
+ assertNull(state.stepSize);
+ assertEquals(-0.7d, state.offset, 0.001d);
+ }
+
+ @Test
+ void testHandleCommandTemperatureOffsetServiceQuantityTypeFahrenheitExceedMaxOffset()
+ throws InterruptedException, TimeoutException, ExecutionException {
+ getFixture().handleCommand(
+ new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_TEMPERATURE_OFFSET),
+ new QuantityType<>(10d, ImperialUnits.FAHRENHEIT));
+ verify(getBridgeHandler()).putState(eq(getDeviceID()),
+ eq(TemperatureOffsetService.TEMPERATURE_OFFSET_SERVICE_NAME), temperatureOffsetCaptor.capture());
+ TemperatureOffsetServiceState state = temperatureOffsetCaptor.getValue();
+ assertNull(state.minOffset);
+ assertNull(state.maxOffset);
+ assertNull(state.stepSize);
+ assertEquals(5d, state.offset, 0.001d);
+ }
+
+ @Test
+ void testHandleCommandTemperatureOffsetServiceQuantityTypeKelvin()
+ throws InterruptedException, TimeoutException, ExecutionException {
+ getFixture().handleCommand(
+ new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_TEMPERATURE_OFFSET),
+ new QuantityType<>(-1.3d, Units.KELVIN));
+ verify(getBridgeHandler()).putState(eq(getDeviceID()),
+ eq(TemperatureOffsetService.TEMPERATURE_OFFSET_SERVICE_NAME), temperatureOffsetCaptor.capture());
+ TemperatureOffsetServiceState state = temperatureOffsetCaptor.getValue();
+ assertNull(state.minOffset);
+ assertNull(state.maxOffset);
+ assertNull(state.stepSize);
+ assertEquals(-1.3d, state.offset, 0.001d);
+ }
+
+ @Test
+ void testHandleCommandTemperatureOffsetServiceQuantityTypeDimensionless()
+ throws InterruptedException, TimeoutException, ExecutionException {
+ getFixture().handleCommand(
+ new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_TEMPERATURE_OFFSET),
+ new QuantityType<>(-1.3d, Units.ONE));
+ verify(getBridgeHandler(), times(0)).putState(eq(getDeviceID()),
+ eq(TemperatureOffsetService.TEMPERATURE_OFFSET_SERVICE_NAME), any());
+ }
+
+ @Test
+ void testHandleCommandTemperatureOffsetServiceDecimalType()
+ throws InterruptedException, TimeoutException, ExecutionException {
+ getFixture().handleCommand(
+ new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_TEMPERATURE_OFFSET),
+ new DecimalType(-1.3d));
+ verify(getBridgeHandler()).putState(eq(getDeviceID()),
+ eq(TemperatureOffsetService.TEMPERATURE_OFFSET_SERVICE_NAME), temperatureOffsetCaptor.capture());
+ TemperatureOffsetServiceState state = temperatureOffsetCaptor.getValue();
+ assertNull(state.minOffset);
+ assertNull(state.maxOffset);
+ assertNull(state.stepSize);
+ assertEquals(-1.3d, state.offset, 0.001d);
+ }
+
+ @Test
+ void testHandleCommandTemperatureOffsetServiceInvalidCommand()
+ throws InterruptedException, TimeoutException, ExecutionException {
+ getFixture().handleCommand(
+ new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_TEMPERATURE_OFFSET), OnOffType.ON);
+ verify(getBridgeHandler(), times(0)).putState(eq(getDeviceID()),
+ eq(TemperatureOffsetService.TEMPERATURE_OFFSET_SERVICE_NAME), any());
+ }
}
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/intrusion/IntrusionDetectionControlStateServiceTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/intrusion/IntrusionDetectionControlStateServiceTest.java
index 24d29ae98f..ed476ca836 100644
--- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/intrusion/IntrusionDetectionControlStateServiceTest.java
+++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/intrusion/IntrusionDetectionControlStateServiceTest.java
@@ -78,7 +78,7 @@ class IntrusionDetectionControlStateServiceTest {
}
@Test
- void setState() throws InterruptedException, TimeoutException, ExecutionException {
+ void setState() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
fixture.setState(testState);
verify(bridgeHandler).putState(BoschSHCBindingConstants.SERVICE_INTRUSION_DETECTION,
"IntrusionDetectionControl", testState);
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/temperatureoffset/TemperatureOffsetServiceTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/temperatureoffset/TemperatureOffsetServiceTest.java
new file mode 100644
index 0000000000..c77b7c0d6e
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/temperatureoffset/TemperatureOffsetServiceTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2010-2026 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.temperatureoffset;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.math.BigDecimal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link TemperatureOffsetService}.
+ *
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+class TemperatureOffsetServiceTest {
+
+ @Test
+ void sanitizeOffsetValue() {
+ assertEquals(-5d, TemperatureOffsetService.sanitizeOffsetValue(BigDecimal.valueOf(-6)), 0.001d);
+ assertEquals(-5d, TemperatureOffsetService.sanitizeOffsetValue(BigDecimal.valueOf(-5.1)), 0.001d);
+
+ assertEquals(5d, TemperatureOffsetService.sanitizeOffsetValue(BigDecimal.valueOf(6)), 0.001d);
+ assertEquals(5d, TemperatureOffsetService.sanitizeOffsetValue(BigDecimal.valueOf(5.1)), 0.001d);
+
+ assertEquals(-5d, TemperatureOffsetService.sanitizeOffsetValue(BigDecimal.valueOf(-5)), 0.001d);
+ assertEquals(-5d, TemperatureOffsetService.sanitizeOffsetValue(BigDecimal.valueOf(-5d)), 0.001d);
+
+ assertEquals(5d, TemperatureOffsetService.sanitizeOffsetValue(BigDecimal.valueOf(5)), 0.001d);
+ assertEquals(5d, TemperatureOffsetService.sanitizeOffsetValue(BigDecimal.valueOf(5d)), 0.001d);
+
+ assertEquals(0.6d, TemperatureOffsetService.sanitizeOffsetValue(BigDecimal.valueOf(0.56d)), 0.001d);
+ assertEquals(0.5d, TemperatureOffsetService.sanitizeOffsetValue(BigDecimal.valueOf(0.54)), 0.001d);
+ assertEquals(0.5d, TemperatureOffsetService.sanitizeOffsetValue(BigDecimal.valueOf(0.548)), 0.001d);
+
+ assertEquals(-1.9d, TemperatureOffsetService.sanitizeOffsetValue(BigDecimal.valueOf(-1.9d)), 0.001d);
+ assertEquals(1.9d, TemperatureOffsetService.sanitizeOffsetValue(BigDecimal.valueOf(1.9d)), 0.001d);
+ assertEquals(0d, TemperatureOffsetService.sanitizeOffsetValue(BigDecimal.valueOf(0)), 0.001d);
+ assertEquals(0d, TemperatureOffsetService.sanitizeOffsetValue(BigDecimal.valueOf(0d)), 0.001d);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/temperatureoffset/dto/TemperatureOffsetServiceStateTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/temperatureoffset/dto/TemperatureOffsetServiceStateTest.java
new file mode 100644
index 0000000000..bad89b4305
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/temperatureoffset/dto/TemperatureOffsetServiceStateTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2010-2026 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.temperatureoffset.dto;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.boschshc.internal.serialization.GsonUtils;
+
+/**
+ * Unit tests for {@link TemperatureOffsetServiceState}.
+ *
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+class TemperatureOffsetServiceStateTest {
+ @Test
+ void testSerialization() {
+ TemperatureOffsetServiceState state = new TemperatureOffsetServiceState();
+ String json = GsonUtils.DEFAULT_GSON_INSTANCE.toJson(state);
+ assertTrue(json.contains("offset"));
+ assertTrue(json.contains("0.0"));
+
+ // make sure the nullable Double objects are not serialized
+ assertFalse(json.contains("stepSize"));
+ assertFalse(json.contains("minOffset"));
+ assertFalse(json.contains("maxOffset"));
+ }
+}