diff --git a/CODEOWNERS b/CODEOWNERS
index 215180a43ae49..83ad37dac608e 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -328,6 +328,7 @@
/bundles/org.openhab.binding.radiobrowser/ @skinah
/bundles/org.openhab.binding.radiothermostat/ @mlobstein
/bundles/org.openhab.binding.regoheatpump/ @crnjan
+/bundles/org.openhab.binding.remehaheating/ @FreddyFFM
/bundles/org.openhab.binding.remoteopenhab/ @lolodomo
/bundles/org.openhab.binding.renault/ @dougculnane
/bundles/org.openhab.binding.resol/ @ramack
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index e67c844d38a74..520e75160ec71 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -1616,6 +1616,11 @@
org.openhab.binding.regoheatpump
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.remehaheating
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.remoteopenhab
diff --git a/bundles/org.openhab.binding.remehaheating/NOTICE b/bundles/org.openhab.binding.remehaheating/NOTICE
new file mode 100644
index 0000000000000..38d625e349232
--- /dev/null
+++ b/bundles/org.openhab.binding.remehaheating/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.remehaheating/README.md b/bundles/org.openhab.binding.remehaheating/README.md
new file mode 100644
index 0000000000000..8d3852c8b9e7c
--- /dev/null
+++ b/bundles/org.openhab.binding.remehaheating/README.md
@@ -0,0 +1,145 @@
+# RemehaHeating Binding
+
+This binding integrates Remeha Home heating systems with openHAB.
+It connects to the Remeha cloud service using the same API as the official Remeha Home mobile app.
+
+The binding supports monitoring and control of Remeha boilers that are connected to the Remeha Home cloud service.
+This includes most modern Remeha boilers with internet connectivity.
+
+Key features include:
+
+- Real-time monitoring of room and outdoor temperatures
+- Target temperature control
+- Hot water (DHW) temperature monitoring and mode control
+- Water pressure monitoring and status
+- System error status monitoring
+
+## Supported Things
+
+This binding supports Remeha boilers that are connected to the Remeha Home cloud service.
+
+- `boiler`: Represents a Remeha boiler with ThingTypeUID `remehaheating:boiler`
+
+The binding has been tested with Remeha Calenta Ace boilers but should work with any Remeha boiler that supports the Remeha Home cloud service.
+
+## Discovery
+
+This binding does not support automatic discovery.
+Boilers must be manually configured using your Remeha Home account credentials.
+
+Each Remeha Home account typically manages one heating system, so you will need one Thing configuration per account.
+
+## Binding Configuration
+
+This binding does not require any global configuration.
+All configuration is done at the Thing level using your Remeha Home account credentials.
+
+## Thing Configuration
+
+To configure a Remeha boiler, you need your Remeha Home account credentials.
+These are the same credentials you use for the Remeha Home mobile app.
+
+### `boiler` Thing Configuration
+
+| Name | Type | Description | Default | Required | Advanced |
+|-----------------|---------|-----------------------------------------------|---------|----------|----------|
+| email | text | Remeha Home account email address | N/A | yes | no |
+| password | text | Remeha Home account password | N/A | yes | no |
+| refreshInterval | integer | Interval the device is polled in seconds | 60 | no | yes |
+
+The refresh interval should be set between 30 and 3600 seconds.
+A shorter interval provides more up-to-date data but may increase API usage.
+
+## Channels
+
+The binding provides the following channels for monitoring and controlling your Remeha heating system:
+
+| Channel | Type | Read/Write | Description |
+|---------------------|-------------------|------------|------------------------------------------------|
+| room-temperature | Number:Temperature| Read | Current room temperature |
+| target-temperature | Number:Temperature| Read/Write | Target room temperature (5-30°C) |
+| dhw-temperature | Number:Temperature| Read | Current hot water temperature |
+| dhw-target | Number:Temperature| Read | Target hot water temperature |
+| dhw-mode | String | Read/Write | DHW mode (anti-frost/schedule/continuous-comfort) |
+| dhw-status | String | Read | Hot water status |
+| water-pressure | Number:Pressure | Read | System water pressure |
+| water-pressure-ok | Switch | Read | Water pressure status (ON=OK, OFF=Low) |
+| outdoor-temperature | Number:Temperature| Read | Outdoor temperature |
+| status | String | Read | Boiler error status |
+
+## Full Example
+
+### Thing Configuration
+
+```java
+Thing remehaheating:boiler:myboiler "Remeha Boiler" [
+ email="",
+ password="",
+ refreshInterval=60
+]
+```
+
+### Item Configuration
+
+```java
+// Temperature monitoring
+Number:Temperature RoomTemp "Room Temperature [%.1f °C]" { channel="remehaheating:boiler:myboiler:room-temperature" }
+Number:Temperature TargetTemp "Target Temperature [%.1f °C]" { channel="remehaheating:boiler:myboiler:target-temperature" }
+Number:Temperature OutdoorTemp "Outdoor Temperature [%.1f °C]" { channel="remehaheating:boiler:myboiler:outdoor-temperature" }
+
+// Hot water
+Number:Temperature DHWTemp "Hot Water Temperature [%.1f °C]" { channel="remehaheating:boiler:myboiler:dhw-temperature" }
+Number:Temperature DHWTarget "Hot Water Target [%.1f °C]" { channel="remehaheating:boiler:myboiler:dhw-target" }
+String DHWMode "Hot Water Mode [%s]" { channel="remehaheating:boiler:myboiler:dhw-mode" }
+String DHWStatus "Hot Water Status [%s]" { channel="remehaheating:boiler:myboiler:dhw-status" }
+
+// System status
+Number:Pressure WaterPressure "Water Pressure [%.1f bar]" { channel="remehaheating:boiler:myboiler:water-pressure" }
+Switch WaterPressureOK "Water Pressure OK" { channel="remehaheating:boiler:myboiler:water-pressure-ok" }
+String BoilerStatus "Boiler Status [%s]" { channel="remehaheating:boiler:myboiler:status" }
+```
+
+### Sitemap Configuration
+
+```perl
+sitemap remeha label="Remeha Heating" {
+ Frame label="Temperature Control" {
+ Text item=RoomTemp
+ Setpoint item=TargetTemp minValue=5 maxValue=30 step=0.5
+ Text item=OutdoorTemp
+ }
+ Frame label="Hot Water" {
+ Text item=DHWTemp
+ Text item=DHWTarget
+ Selection item=DHWMode mappings=["anti-frost"="Anti-frost", "schedule"="Schedule", "continuous-comfort"="Continuous Comfort"]
+ Text item=DHWStatus
+ }
+ Frame label="System Status" {
+ Text item=WaterPressure
+ Text item=WaterPressureOK
+ Text item=BoilerStatus
+ }
+}
+```
+
+## Authentication
+
+This binding uses the same OAuth2 PKCE authentication flow as the official Remeha Home mobile app.
+Your credentials are used only to obtain an access token and are not stored permanently.
+
+The binding automatically handles token refresh and re-authentication as needed.
+
+## Limitations
+
+- Only the first appliance from your Remeha Home account is supported
+- Only the first climate zone and hot water zone are monitored
+- The binding requires an active internet connection to the Remeha cloud service
+- API rate limiting may apply - avoid setting very short refresh intervals
+
+## Troubleshooting
+
+- Ensure your Remeha Home account credentials are correct
+- Check that your boiler is online in the Remeha Home mobile app
+- Verify your openHAB system has internet connectivity
+- Check the openHAB logs for authentication or API errors
+- Try increasing the refresh interval if you experience connection issues
diff --git a/bundles/org.openhab.binding.remehaheating/pom.xml b/bundles/org.openhab.binding.remehaheating/pom.xml
new file mode 100644
index 0000000000000..c523d0d1c82a0
--- /dev/null
+++ b/bundles/org.openhab.binding.remehaheating/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 5.2.0-SNAPSHOT
+
+
+ org.openhab.binding.remehaheating
+
+ openHAB Add-ons :: Bundles :: RemehaHeating Binding
+
+
diff --git a/bundles/org.openhab.binding.remehaheating/src/main/feature/feature.xml b/bundles/org.openhab.binding.remehaheating/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..c48ebfb4c704d
--- /dev/null
+++ b/bundles/org.openhab.binding.remehaheating/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.remehaheating/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.remehaheating/src/main/java/org/openhab/binding/remehaheating/internal/RemehaHeatingBindingConstants.java b/bundles/org.openhab.binding.remehaheating/src/main/java/org/openhab/binding/remehaheating/internal/RemehaHeatingBindingConstants.java
new file mode 100644
index 0000000000000..cb5caf71e05e1
--- /dev/null
+++ b/bundles/org.openhab.binding.remehaheating/src/main/java/org/openhab/binding/remehaheating/internal/RemehaHeatingBindingConstants.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2010-2025 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.remehaheating.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link RemehaHeatingBindingConstants} class defines common constants used across the binding.
+ *
+ * This class contains:
+ * - Thing type UIDs for supported devices
+ * - Channel identifiers for all supported channels
+ * - Configuration parameter names
+ * - DHW mode constants
+ *
+ * @author Michael Fraedrich - Initial contribution
+ */
+@NonNullByDefault
+public class RemehaHeatingBindingConstants {
+
+ private static final String BINDING_ID = "remehaheating";
+
+ // Thing Type UIDs
+ /** Thing type UID for Remeha boiler */
+ public static final ThingTypeUID THING_TYPE_BOILER = new ThingTypeUID(BINDING_ID, "boiler");
+
+ // Channel identifiers
+ /** Current room temperature channel */
+ public static final String CHANNEL_ROOM_TEMPERATURE = "room-temperature";
+ /** Target room temperature channel (read/write) */
+ public static final String CHANNEL_TARGET_TEMPERATURE = "target-temperature";
+ /** Current DHW temperature channel */
+ public static final String CHANNEL_DHW_TEMPERATURE = "dhw-temperature";
+ /** Target DHW temperature channel */
+ public static final String CHANNEL_DHW_TARGET = "dhw-target";
+ /** System water pressure channel */
+ public static final String CHANNEL_WATER_PRESSURE = "water-pressure";
+ /** Outdoor temperature channel */
+ public static final String CHANNEL_OUTDOOR_TEMPERATURE = "outdoor-temperature";
+ /** Boiler error status channel */
+ public static final String CHANNEL_STATUS = "status";
+ /** DHW operating mode channel (read/write) */
+ public static final String CHANNEL_DHW_MODE = "dhw-mode";
+ /** Water pressure OK status channel */
+ public static final String CHANNEL_WATER_PRESSURE_OK = "water-pressure-ok";
+ /** DHW status channel */
+ public static final String CHANNEL_DHW_STATUS = "dhw-status";
+
+ // Configuration parameter names
+ /** Email configuration parameter */
+ public static final String CONFIG_EMAIL = "email";
+ /** Password configuration parameter */
+ public static final String CONFIG_PASSWORD = "password";
+ /** Refresh interval configuration parameter */
+ public static final String CONFIG_REFRESH_INTERVAL = "refreshInterval";
+
+ // DHW operating modes
+ /** Anti-frost DHW mode - minimal heating to prevent freezing */
+ public static final String DHW_MODE_ANTI_FROST = "anti-frost";
+ /** Schedule DHW mode - follows programmed schedule */
+ public static final String DHW_MODE_SCHEDULE = "schedule";
+ /** Continuous comfort DHW mode - maintains target temperature */
+ public static final String DHW_MODE_CONTINUOUS_COMFORT = "continuous-comfort";
+}
diff --git a/bundles/org.openhab.binding.remehaheating/src/main/java/org/openhab/binding/remehaheating/internal/RemehaHeatingConfiguration.java b/bundles/org.openhab.binding.remehaheating/src/main/java/org/openhab/binding/remehaheating/internal/RemehaHeatingConfiguration.java
new file mode 100644
index 0000000000000..15a6d1e447553
--- /dev/null
+++ b/bundles/org.openhab.binding.remehaheating/src/main/java/org/openhab/binding/remehaheating/internal/RemehaHeatingConfiguration.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2010-2025 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.remehaheating.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link RemehaHeatingConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * This configuration class holds the parameters required to connect to a Remeha Home account:
+ * - Email and password for authentication
+ * - Refresh interval for periodic data updates
+ *
+ * @author Michael Fraedrich - Initial contribution
+ */
+@NonNullByDefault
+public class RemehaHeatingConfiguration {
+
+ /**
+ * Remeha Home account email address.
+ * This is the same email used for the Remeha Home mobile app.
+ */
+ public String email = "";
+
+ /**
+ * Remeha Home account password.
+ * This is the same password used for the Remeha Home mobile app.
+ */
+ public String password = "";
+
+ /**
+ * Refresh interval in seconds for polling the Remeha API.
+ * Default is 60 seconds. Valid range is 30-3600 seconds.
+ */
+ public int refreshInterval = 60;
+}
diff --git a/bundles/org.openhab.binding.remehaheating/src/main/java/org/openhab/binding/remehaheating/internal/RemehaHeatingHandler.java b/bundles/org.openhab.binding.remehaheating/src/main/java/org/openhab/binding/remehaheating/internal/RemehaHeatingHandler.java
new file mode 100644
index 0000000000000..da962e0ba5be2
--- /dev/null
+++ b/bundles/org.openhab.binding.remehaheating/src/main/java/org/openhab/binding/remehaheating/internal/RemehaHeatingHandler.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright (c) 2010-2025 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.remehaheating.internal;
+
+import static org.openhab.binding.remehaheating.internal.RemehaHeatingBindingConstants.*;
+
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.remehaheating.internal.api.RemehaApiClient;
+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.types.StringType;
+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.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+
+/**
+ * The {@link RemehaHeatingHandler} handles communication with Remeha Home heating systems.
+ *
+ * This handler manages the connection to the Remeha API, authenticates using OAuth2 PKCE flow,
+ * and provides access to heating system data including temperatures, water pressure, and DHW controls.
+ *
+ * Supported features:
+ * - Room and outdoor temperature monitoring
+ * - Target temperature control
+ * - Hot water temperature and status monitoring
+ * - DHW mode control (anti-frost, schedule, continuous-comfort)
+ * - Water pressure monitoring
+ * - System status monitoring
+ *
+ * @author Michael Fraedrich - Initial contribution
+ */
+@NonNullByDefault
+public class RemehaHeatingHandler extends BaseThingHandler {
+
+ private final Logger logger = LoggerFactory.getLogger(RemehaHeatingHandler.class);
+ private final HttpClient httpClient;
+ private @Nullable RemehaApiClient apiClient;
+ private @Nullable ScheduledFuture> refreshJob;
+
+ public RemehaHeatingHandler(Thing thing, HttpClient httpClient) {
+ super(thing);
+ this.httpClient = httpClient;
+ }
+
+ /**
+ * Handles commands sent to the binding channels.
+ *
+ * Supported commands:
+ * - RefreshType: Updates all channel states from API
+ * - DecimalType on targetTemperature: Sets new target room temperature
+ * - StringType on dhwMode: Changes DHW mode (anti-frost/schedule/continuous-comfort)
+ */
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (command instanceof RefreshType) {
+ updateData();
+ } else if (CHANNEL_TARGET_TEMPERATURE.equals(channelUID.getId())) {
+ if (command instanceof QuantityType> qt) {
+ QuantityType> celsius = qt.toUnit(SIUnits.CELSIUS);
+ if (celsius != null) {
+ setTargetTemperature(celsius.doubleValue());
+ }
+ } else if (command instanceof DecimalType dt) {
+ setTargetTemperature(dt.doubleValue());
+ }
+ } else if (CHANNEL_DHW_MODE.equals(channelUID.getId()) && command instanceof StringType) {
+ setDhwMode(command.toString());
+ }
+ }
+
+ /**
+ * Initializes the handler by validating configuration and authenticating with Remeha API.
+ * Sets up periodic data refresh job on successful authentication.
+ */
+ @Override
+ public void initialize() {
+ try {
+ RemehaHeatingConfiguration config = getConfigAs(RemehaHeatingConfiguration.class);
+ String email = config.email;
+ String password = config.password;
+ int refreshInterval = config.refreshInterval;
+
+ if (email.isBlank() || password.isBlank()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "@text/offline.conf-error-no-credentials");
+ return;
+ }
+
+ updateStatus(ThingStatus.UNKNOWN);
+ apiClient = new RemehaApiClient(httpClient);
+
+ scheduler.execute(() -> authenticateAndStart(email, password, refreshInterval));
+ } catch (IllegalArgumentException e) {
+ logger.debug("Invalid configuration", e);
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "@text/offline.conf-error-invalid-config");
+ }
+ }
+
+ private void authenticateAndStart(String email, String password, int refreshInterval) {
+ try {
+ RemehaApiClient client = apiClient;
+ if (client != null && client.authenticate(email, password)) {
+ updateStatus(ThingStatus.ONLINE);
+ startRefreshJob(refreshInterval > 0 ? refreshInterval : 60);
+ } else {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/offline.comm-error-authentication-failed");
+ }
+ } catch (RuntimeException e) {
+ logger.debug("Authentication error", e);
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/offline.comm-error-authentication-error");
+ }
+ }
+
+ /**
+ * Cleans up resources when the handler is disposed.
+ * Stops the refresh job.
+ */
+ @Override
+ public void dispose() {
+ stopRefreshJob();
+ apiClient = null;
+ try {
+ if (httpClient.isStarted()) {
+ httpClient.stop();
+ }
+ } catch (Exception e) {
+ logger.debug("Error stopping HTTP client", e);
+ }
+ super.dispose();
+ }
+
+ /**
+ * Starts the periodic data refresh job.
+ *
+ * @param intervalSeconds Refresh interval in seconds
+ */
+ private void startRefreshJob(int intervalSeconds) {
+ refreshJob = scheduler.scheduleWithFixedDelay(this::updateData, 0, intervalSeconds, TimeUnit.SECONDS);
+ }
+
+ /**
+ * Stops the periodic data refresh job if running.
+ */
+ private void stopRefreshJob() {
+ ScheduledFuture> job = refreshJob;
+ if (job != null) {
+ job.cancel(true);
+ refreshJob = null;
+ }
+ }
+
+ /**
+ * Fetches latest data from Remeha API and updates all channel states.
+ *
+ * Updates the following channels:
+ * - Room and outdoor temperatures
+ * - Target temperature
+ * - DHW temperature, target, mode, and status
+ * - Water pressure and pressure OK status
+ * - System error status
+ */
+ private void updateData() {
+ RemehaApiClient client = apiClient;
+ if (client == null) {
+ return;
+ }
+
+ try {
+ JsonObject dashboard = client.getDashboard();
+ if (dashboard == null) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/offline.comm-error-data-fetch-failed");
+ return;
+ }
+
+ updateStatus(ThingStatus.ONLINE);
+
+ JsonArray appliances = dashboard.getAsJsonArray("appliances");
+ if (appliances != null && appliances.size() > 0) {
+ JsonObject appliance = appliances.get(0).getAsJsonObject();
+
+ // Update channels with proper units
+ double pressure = appliance.get("waterPressure").getAsDouble();
+ logger.debug("Updating water pressure: {} bar", pressure);
+ updateState(CHANNEL_WATER_PRESSURE, new QuantityType<>(pressure, Units.BAR));
+ updateState(CHANNEL_STATUS, new StringType(appliance.get("errorStatus").getAsString()));
+ updateState(CHANNEL_WATER_PRESSURE_OK, OnOffType.from(appliance.get("waterPressureOK").getAsBoolean()));
+
+ JsonObject outdoorInfo = appliance.getAsJsonObject("outdoorTemperatureInformation");
+ if (outdoorInfo != null) {
+ double temp = outdoorInfo.get("internetOutdoorTemperature").getAsDouble();
+ updateState(CHANNEL_OUTDOOR_TEMPERATURE, new QuantityType<>(temp, SIUnits.CELSIUS));
+ }
+
+ // Climate zones
+ JsonArray climateZones = appliance.getAsJsonArray("climateZones");
+ if (climateZones != null && climateZones.size() > 0) {
+ JsonObject zone = climateZones.get(0).getAsJsonObject();
+ double roomTemp = zone.get("roomTemperature").getAsDouble();
+ double targetTemp = zone.get("setPoint").getAsDouble();
+ updateState(CHANNEL_ROOM_TEMPERATURE, new QuantityType<>(roomTemp, SIUnits.CELSIUS));
+ updateState(CHANNEL_TARGET_TEMPERATURE, new QuantityType<>(targetTemp, SIUnits.CELSIUS));
+ }
+
+ // Hot water zones
+ JsonArray hotWaterZones = appliance.getAsJsonArray("hotWaterZones");
+ if (hotWaterZones != null && hotWaterZones.size() > 0) {
+ JsonObject zone = hotWaterZones.get(0).getAsJsonObject();
+ double dhwTemp = zone.get("dhwTemperature").getAsDouble();
+ double dhwTarget = zone.get("targetSetpoint").getAsDouble();
+ String dhwMode = zone.get("dhwZoneMode").getAsString();
+ String dhwStatus = zone.get("dhwStatus").getAsString();
+ updateState(CHANNEL_DHW_TEMPERATURE, new QuantityType<>(dhwTemp, SIUnits.CELSIUS));
+ updateState(CHANNEL_DHW_TARGET, new QuantityType<>(dhwTarget, SIUnits.CELSIUS));
+ updateState(CHANNEL_DHW_MODE, new StringType(dhwMode));
+ updateState(CHANNEL_DHW_STATUS, new StringType(dhwStatus));
+ }
+ }
+ } catch (IllegalStateException | NullPointerException e) {
+ logger.debug("Error updating data", e);
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+ "@text/offline.comm-error-data-update-failed");
+ }
+ }
+
+ /**
+ * Sets the target room temperature via API.
+ *
+ * @param temperature Target temperature in Celsius
+ */
+ private void setTargetTemperature(double temperature) {
+ RemehaApiClient client = apiClient;
+ if (client != null) {
+ String climateZoneId = getClimateZoneId();
+ if (climateZoneId != null) {
+ if (!client.setTemperature(climateZoneId, temperature)) {
+ logger.debug("Failed to set target temperature");
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets the DHW (Domestic Hot Water) mode via API.
+ *
+ * @param mode DHW mode: "anti-frost", "schedule", or "continuous-comfort"
+ */
+ private void setDhwMode(String mode) {
+ RemehaApiClient client = apiClient;
+ if (client != null) {
+ String hotWaterZoneId = getHotWaterZoneId();
+ if (hotWaterZoneId != null) {
+ if (!client.setDhwMode(hotWaterZoneId, mode)) {
+ logger.debug("Failed to set DHW mode");
+ }
+ }
+ }
+ }
+
+ /**
+ * Retrieves the climate zone ID from the dashboard data.
+ * Used for temperature control API calls.
+ *
+ * @return Climate zone ID or null if not available
+ */
+ private @Nullable String getClimateZoneId() {
+ RemehaApiClient client = apiClient;
+ if (client == null) {
+ return null;
+ }
+
+ try {
+ JsonObject dashboard = client.getDashboard();
+ if (dashboard != null) {
+ JsonArray appliances = dashboard.getAsJsonArray("appliances");
+ if (appliances != null && appliances.size() > 0) {
+ JsonObject appliance = appliances.get(0).getAsJsonObject();
+ JsonArray climateZones = appliance.getAsJsonArray("climateZones");
+ if (climateZones != null && climateZones.size() > 0) {
+ return climateZones.get(0).getAsJsonObject().get("climateZoneId").getAsString();
+ }
+ }
+ }
+ } catch (IllegalStateException | NullPointerException e) {
+ logger.debug("Error getting climate zone ID: {}", e.getMessage());
+ }
+ return null;
+ }
+
+ /**
+ * Retrieves the hot water zone ID from the dashboard data.
+ * Used for DHW control API calls.
+ *
+ * @return Hot water zone ID or null if not available
+ */
+ private @Nullable String getHotWaterZoneId() {
+ RemehaApiClient client = apiClient;
+ if (client == null) {
+ return null;
+ }
+
+ try {
+ JsonObject dashboard = client.getDashboard();
+ if (dashboard != null) {
+ JsonArray appliances = dashboard.getAsJsonArray("appliances");
+ if (appliances != null && appliances.size() > 0) {
+ JsonObject appliance = appliances.get(0).getAsJsonObject();
+ JsonArray hotWaterZones = appliance.getAsJsonArray("hotWaterZones");
+ if (hotWaterZones != null && hotWaterZones.size() > 0) {
+ return hotWaterZones.get(0).getAsJsonObject().get("hotWaterZoneId").getAsString();
+ }
+ }
+ }
+ } catch (IllegalStateException | NullPointerException e) {
+ logger.debug("Error getting hot water zone ID: {}", e.getMessage());
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.remehaheating/src/main/java/org/openhab/binding/remehaheating/internal/RemehaHeatingHandlerFactory.java b/bundles/org.openhab.binding.remehaheating/src/main/java/org/openhab/binding/remehaheating/internal/RemehaHeatingHandlerFactory.java
new file mode 100644
index 0000000000000..0f845e3f259a8
--- /dev/null
+++ b/bundles/org.openhab.binding.remehaheating/src/main/java/org/openhab/binding/remehaheating/internal/RemehaHeatingHandlerFactory.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2010-2025 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.remehaheating.internal;
+
+import static org.openhab.binding.remehaheating.internal.RemehaHeatingBindingConstants.THING_TYPE_BOILER;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link RemehaHeatingHandlerFactory} is responsible for creating things and thing handlers.
+ *
+ * This factory creates handlers for supported Remeha heating system things.
+ * It implements the OSGi component pattern and is automatically registered
+ * as a ThingHandlerFactory service.
+ *
+ * Currently supports:
+ * - Remeha boiler things (THING_TYPE_BOILER)
+ *
+ * @author Michael Fraedrich - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.remehaheating", service = ThingHandlerFactory.class)
+public class RemehaHeatingHandlerFactory extends BaseThingHandlerFactory {
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BOILER);
+ private final HttpClientFactory httpClientFactory;
+
+ @Activate
+ public RemehaHeatingHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
+ this.httpClientFactory = httpClientFactory;
+ }
+
+ /**
+ * Checks if this factory supports the given thing type.
+ *
+ * @param thingTypeUID The thing type UID to check
+ * @return true if the thing type is supported, false otherwise
+ */
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ /**
+ * Creates a thing handler for the given thing.
+ *
+ * @param thing The thing for which to create a handler
+ * @return A new handler instance or null if the thing type is not supported
+ */
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+ if (THING_TYPE_BOILER.equals(thingTypeUID)) {
+ HttpClient httpClient = httpClientFactory.createHttpClient("remehaheating");
+ httpClient.setRequestBufferSize(16384);
+ httpClient.setResponseBufferSize(16384);
+ try {
+ httpClient.start();
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to start HTTP client", e);
+ }
+ return new RemehaHeatingHandler(thing, httpClient);
+ }
+
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.remehaheating/src/main/java/org/openhab/binding/remehaheating/internal/api/RemehaApiClient.java b/bundles/org.openhab.binding.remehaheating/src/main/java/org/openhab/binding/remehaheating/internal/api/RemehaApiClient.java
new file mode 100644
index 0000000000000..87387ac277007
--- /dev/null
+++ b/bundles/org.openhab.binding.remehaheating/src/main/java/org/openhab/binding/remehaheating/internal/api/RemehaApiClient.java
@@ -0,0 +1,366 @@
+/*
+ * Copyright (c) 2010-2025 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.remehaheating.internal.api;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.Base64;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpMethod;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+
+/**
+ * The {@link RemehaApiClient} handles OAuth2 PKCE authentication and API communication with Remeha Home services.
+ *
+ * This client implements a custom OAuth2 PKCE authentication flow required by the Remeha API.
+ * The openHAB core OAuth2 client cannot be used because the Remeha API uses Azure B2C with a non-standard
+ * authentication flow that requires:
+ * - CSRF token extraction from authentication page cookies
+ * - Custom state properties (TID) handling for Azure B2C
+ * - Multi-step form submission with CSRF tokens
+ * - Manual authorization code extraction from redirect responses
+ *
+ * The standard OAuth2 Resource Owner Password Credentials flow is not supported by this Azure B2C
+ * configuration, and the authorization code flow requires programmatic interaction with the login form,
+ * which is not possible with the standard OAuth2 client.
+ *
+ * @author Michael Fraedrich - Initial contribution
+ */
+@NonNullByDefault
+public class RemehaApiClient {
+ private final Logger logger = LoggerFactory.getLogger(RemehaApiClient.class);
+ private final HttpClient httpClient;
+ private final Gson gson = new Gson();
+ private @Nullable String accessToken;
+ private String codeVerifier = "";
+ private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+ private static final String API_BASE_URL = "https://api.bdrthermea.net/Mobile/api";
+ private static final String SUBSCRIPTION_KEY = "df605c5470d846fc91e848b1cc653ddf";
+ private static final long REQUEST_TIMEOUT_MS = 30000;
+ private static final Pattern CSRF_PATTERN = Pattern.compile("x-ms-cpim-csrf=([^;]+)");
+
+ /**
+ * Creates a new RemehaApiClient with the provided HttpClient.
+ *
+ * Note: This client requires custom buffer sizes (16384 bytes) to handle large OAuth2 responses
+ * from Azure B2C authentication. The HttpClient should be created via HttpClientFactory.createHttpClient()
+ * with buffer sizes configured in the factory, not using the common HTTP client.
+ *
+ * @param httpClient HttpClient instance with appropriate buffer sizes configured
+ */
+ public RemehaApiClient(HttpClient httpClient) {
+ this.httpClient = httpClient;
+ }
+
+ /**
+ * Authenticates with Remeha API using OAuth2 PKCE flow.
+ *
+ * This method performs the complete authentication sequence:
+ * 1. Generates PKCE code verifier and challenge
+ * 2. Initiates OAuth2 authorization request
+ * 3. Extracts CSRF token from response cookies
+ * 4. Submits user credentials
+ * 5. Retrieves authorization code from redirect
+ * 6. Exchanges authorization code for access token
+ *
+ * @param email Remeha Home account email
+ * @param password Remeha Home account password
+ * @return true if authentication successful, false otherwise
+ */
+ public boolean authenticate(String email, String password) {
+ try {
+ codeVerifier = generateRandomString();
+ String codeChallenge = generateCodeChallenge(codeVerifier);
+ String state = generateRandomString();
+
+ String authUrl = buildAuthUrl(codeChallenge, state);
+ Request authRequest = httpClient.newRequest(authUrl).method(HttpMethod.GET).timeout(REQUEST_TIMEOUT_MS,
+ TimeUnit.MILLISECONDS);
+
+ ContentResponse response = authRequest.send();
+ String requestId = response.getHeaders().get("x-request-id");
+ String csrfToken = extractCsrfToken(response);
+
+ if (csrfToken == null || requestId == null) {
+ logger.debug("Failed to extract CSRF token or request ID");
+ return false;
+ }
+
+ String stateProperties = createStateProperties(requestId);
+ if (!submitCredentials(email, password, csrfToken, stateProperties)) {
+ return false;
+ }
+
+ String authCode = getAuthorizationCode(csrfToken, stateProperties);
+ if (authCode == null) {
+ return false;
+ }
+
+ return exchangeCodeForToken(authCode);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.debug("Authentication interrupted", e);
+ return false;
+ } catch (Exception e) {
+ logger.debug("Authentication failed", e);
+ return false;
+ }
+ }
+
+ /**
+ * Retrieves the dashboard data containing all heating system information.
+ *
+ * The dashboard includes:
+ * - Appliance information (boiler status, water pressure)
+ * - Climate zones (room temperature, target temperature)
+ * - Hot water zones (DHW temperature, mode, status)
+ * - Outdoor temperature information
+ *
+ * @return Dashboard JSON object or null if request fails
+ */
+ public @Nullable JsonObject getDashboard() {
+ if (accessToken == null) {
+ return null;
+ }
+ try {
+ ContentResponse response = httpClient.newRequest(API_BASE_URL + "/homes/dashboard").method(HttpMethod.GET)
+ .timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS).header("Authorization", "Bearer " + accessToken)
+ .header("Ocp-Apim-Subscription-Key", SUBSCRIPTION_KEY).send();
+ if (response.getStatus() == 401) {
+ logger.debug("Received 401 Unauthorized, token expired");
+ accessToken = null;
+ return null;
+ }
+ return gson.fromJson(response.getContentAsString(), JsonObject.class);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.debug("Dashboard request interrupted", e);
+ return null;
+ } catch (Exception e) {
+ logger.debug("Failed to get dashboard", e);
+ return null;
+ }
+ }
+
+ /**
+ * Sets the target room temperature for a climate zone.
+ *
+ * @param climateZoneId Climate zone identifier from dashboard data
+ * @param temperature Target temperature in Celsius
+ * @return true if request successful, false otherwise
+ */
+ public boolean setTemperature(String climateZoneId, double temperature) {
+ return apiRequest("/climate-zones/" + climateZoneId + "/modes/manual",
+ "{\"roomTemperatureSetPoint\":" + temperature + "}");
+ }
+
+ /**
+ * Sets the DHW (Domestic Hot Water) operating mode.
+ *
+ * @param hotWaterZoneId Hot water zone identifier from dashboard data
+ * @param mode DHW mode: "anti-frost", "schedule", or "continuous-comfort"
+ * @return true if request successful, false otherwise
+ */
+ public boolean setDhwMode(String hotWaterZoneId, String mode) {
+ return apiRequest("/hot-water-zones/" + hotWaterZoneId + "/modes/" + mode, null);
+ }
+
+ private String generateCodeChallenge(String verifier) throws Exception {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hash = digest.digest(verifier.getBytes(StandardCharsets.UTF_8));
+ return Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
+ }
+
+ private String generateRandomString() {
+ byte[] bytes = new byte[32];
+ SECURE_RANDOM.nextBytes(bytes);
+ return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
+ }
+
+ private String buildAuthUrl(String codeChallenge, String state) {
+ return "https://remehalogin.bdrthermea.net/bdrb2cprod.onmicrosoft.com/oauth2/v2.0/authorize"
+ + "?response_type=code" + "&client_id=6ce007c6-0628-419e-88f4-bee2e6418eec" + "&redirect_uri="
+ + URLEncoder.encode("com.b2c.remehaapp://login-callback", StandardCharsets.UTF_8) + "&scope="
+ + URLEncoder.encode(
+ "openid https://bdrb2cprod.onmicrosoft.com/iotdevice/user_impersonation offline_access",
+ StandardCharsets.UTF_8)
+ + "&state=" + state + "&code_challenge=" + codeChallenge + "&code_challenge_method=S256"
+ + "&p=B2C_1A_RPSignUpSignInNewRoomV3.1" + "&brand=remeha" + "&lang=en" + "&nonce=defaultNonce"
+ + "&prompt=login" + "&signUp=False";
+ }
+
+ private @Nullable String extractCsrfToken(ContentResponse response) {
+ HttpFields headers = response.getHeaders();
+ logger.debug("Extracting CSRF token from cookies");
+ for (String setCookieHeader : headers.getValuesList("Set-Cookie")) {
+ if (setCookieHeader != null && setCookieHeader.contains("x-ms-cpim-csrf=")) {
+ Matcher matcher = CSRF_PATTERN.matcher(setCookieHeader);
+ if (matcher.find()) {
+ String token = matcher.group(1);
+ logger.debug("CSRF token extracted from cookies");
+ return token;
+ }
+ }
+ }
+ logger.debug("No CSRF token found in cookies");
+ return null;
+ }
+
+ private String createStateProperties(String requestId) {
+ String json = "{\"TID\":\"" + requestId + "\"}";
+ return Base64.getUrlEncoder().withoutPadding().encodeToString(json.getBytes(StandardCharsets.UTF_8));
+ }
+
+ private boolean submitCredentials(String email, String password, String csrfToken, String stateProperties) {
+ try {
+ String baseUrl = "https://remehalogin.bdrthermea.net/bdrb2cprod.onmicrosoft.com/B2C_1A_RPSignUpSignInNewRoomv3.1/SelfAsserted";
+
+ String formData = "request_type=RESPONSE" + "&signInName="
+ + URLEncoder.encode(email, StandardCharsets.UTF_8) + "&password="
+ + URLEncoder.encode(password, StandardCharsets.UTF_8);
+
+ logger.debug("Submitting credentials with CSRF token");
+
+ Request request = httpClient.newRequest(baseUrl).method(HttpMethod.POST)
+ .timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ .param("tx", "StateProperties=" + stateProperties).param("p", "B2C_1A_RPSignUpSignInNewRoomv3.1")
+ .header("x-csrf-token", csrfToken).header("Content-Type", "application/x-www-form-urlencoded")
+ .content(new StringContentProvider(formData));
+
+ ContentResponse response = request.send();
+ int status = response.getStatus();
+ logger.debug("Submit credentials response: {}", status);
+ if (status != 200) {
+ logger.debug("Credential submission failed with status: {}", status);
+ }
+ return status == 200;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.debug("Credential submission interrupted", e);
+ return false;
+ } catch (Exception e) {
+ logger.debug("Failed to submit credentials", e);
+ return false;
+ }
+ }
+
+ private @Nullable String getAuthorizationCode(String csrfToken, String stateProperties) {
+ try {
+ String baseUrl = "https://remehalogin.bdrthermea.net/bdrb2cprod.onmicrosoft.com/B2C_1A_RPSignUpSignInNewRoomv3.1/api/CombinedSigninAndSignup/confirmed";
+
+ Request request = httpClient.newRequest(baseUrl).method(HttpMethod.GET)
+ .timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS).param("rememberMe", "false")
+ .param("csrf_token", csrfToken).param("tx", "StateProperties=" + stateProperties)
+ .param("p", "B2C_1A_RPSignUpSignInNewRoomv3.1").followRedirects(false);
+
+ ContentResponse response = request.send();
+ logger.debug("Authorization code response status: {}", response.getStatus());
+
+ if (response.getStatus() == 302) {
+ String location = response.getHeaders().get("Location");
+ logger.debug("Redirect location: {}", location);
+ if (location != null) {
+ Pattern pattern = Pattern.compile("code=([^&]+)");
+ Matcher matcher = pattern.matcher(location);
+ if (matcher.find()) {
+ String authCode = matcher.group(1);
+ logger.debug("Authorization code successfully extracted.");
+ return authCode;
+ }
+ }
+ } else {
+ logger.debug("Expected 302 redirect, got {}", response.getStatus());
+ }
+ } catch (Exception e) {
+ logger.debug("Failed to get authorization code: {}", e.getMessage());
+ }
+ return null;
+ }
+
+ private boolean exchangeCodeForToken(String authCode) {
+ try {
+ String url = "https://remehalogin.bdrthermea.net/bdrb2cprod.onmicrosoft.com/oauth2/v2.0/token?p=B2C_1A_RPSignUpSignInNewRoomV3.1";
+ String formData = "grant_type=authorization_code&code=" + authCode + "&redirect_uri="
+ + URLEncoder.encode("com.b2c.remehaapp://login-callback", StandardCharsets.UTF_8)
+ + "&code_verifier=" + codeVerifier + "&client_id=6ce007c6-0628-419e-88f4-bee2e6418eec";
+
+ Request request = httpClient.newRequest(url).method(HttpMethod.POST)
+ .timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .content(new StringContentProvider(formData));
+
+ ContentResponse response = request.send();
+ if (response.getStatus() == 200) {
+ String json = response.getContentAsString();
+ JsonObject tokenResponse = gson.fromJson(json, JsonObject.class);
+ if (tokenResponse != null && tokenResponse.has("access_token")) {
+ accessToken = tokenResponse.get("access_token").getAsString();
+ logger.debug("Successfully obtained access token");
+ return true;
+ } else {
+ logger.debug("Token response missing access_token field");
+ }
+ } else {
+ logger.debug("Token exchange failed with status: {}", response.getStatus());
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.debug("Token exchange interrupted", e);
+ } catch (java.util.concurrent.TimeoutException e) {
+ logger.debug("Token exchange timed out after {}ms", REQUEST_TIMEOUT_MS, e);
+ } catch (Exception e) {
+ logger.debug("Failed to exchange code for token: {}", e.getMessage());
+ }
+ return false;
+ }
+
+ private boolean apiRequest(String path, @Nullable String jsonData) {
+ if (accessToken == null) {
+ return false;
+ }
+ try {
+ Request request = httpClient.newRequest(API_BASE_URL + path).method(HttpMethod.POST)
+ .timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS).header("Authorization", "Bearer " + accessToken)
+ .header("Ocp-Apim-Subscription-Key", SUBSCRIPTION_KEY).header("Content-Type", "application/json");
+ if (jsonData != null) {
+ request.content(new StringContentProvider(jsonData));
+ }
+ int status = request.send().getStatus();
+ if (status == 401) {
+ logger.debug("Received 401 Unauthorized, token expired");
+ accessToken = null;
+ return false;
+ }
+ return status == 200;
+ } catch (Exception e) {
+ logger.debug("API request failed for {}: {}", path, e.getMessage());
+ return false;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.remehaheating/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.remehaheating/src/main/resources/OH-INF/addon/addon.xml
new file mode 100644
index 0000000000000..ed58eea54deea
--- /dev/null
+++ b/bundles/org.openhab.binding.remehaheating/src/main/resources/OH-INF/addon/addon.xml
@@ -0,0 +1,12 @@
+
+
+
+ binding
+ Remeha Heating Binding
+ This binding integrates Remeha Home heating systems, allowing control and monitoring of boilers through
+ the Remeha Home cloud service.
+ cloud
+
+
diff --git a/bundles/org.openhab.binding.remehaheating/src/main/resources/OH-INF/i18n/remehaheating.properties b/bundles/org.openhab.binding.remehaheating/src/main/resources/OH-INF/i18n/remehaheating.properties
new file mode 100644
index 0000000000000..f5aed5d6bcfdd
--- /dev/null
+++ b/bundles/org.openhab.binding.remehaheating/src/main/resources/OH-INF/i18n/remehaheating.properties
@@ -0,0 +1,53 @@
+# add-on
+
+addon.remehaheating.name = Remeha Heating Binding
+addon.remehaheating.description = This binding integrates Remeha Home heating systems, allowing control and monitoring of boilers through the Remeha Home cloud service.
+
+# thing types
+
+thing-type.remehaheating.boiler.label = Remeha Boiler
+thing-type.remehaheating.boiler.description = Remeha Home heating system boiler
+
+# thing types config
+
+thing-type.config.remehaheating.boiler.email.label = Email
+thing-type.config.remehaheating.boiler.email.description = Remeha Home account email
+thing-type.config.remehaheating.boiler.password.label = Password
+thing-type.config.remehaheating.boiler.password.description = Remeha Home account password
+thing-type.config.remehaheating.boiler.refreshInterval.label = Refresh Interval
+thing-type.config.remehaheating.boiler.refreshInterval.description = Interval to refresh data from Remeha API (from 30 to 3600 seconds)
+
+# channel types
+
+channel-type.remehaheating.dhw-mode.label = Hot Water Mode
+channel-type.remehaheating.dhw-mode.description = Domestic hot water zone mode
+channel-type.remehaheating.dhw-mode.state.option.anti-frost = Anti-frost
+channel-type.remehaheating.dhw-mode.state.option.schedule = Schedule
+channel-type.remehaheating.dhw-mode.state.option.continuous-comfort = Continuous Comfort
+channel-type.remehaheating.dhw-status.label = Hot Water Status
+channel-type.remehaheating.dhw-status.description = Hot water status
+channel-type.remehaheating.dhw-target.label = Hot Water Target
+channel-type.remehaheating.dhw-target.description = Target hot water temperature
+channel-type.remehaheating.dhw-temperature.label = Hot Water Temperature
+channel-type.remehaheating.dhw-temperature.description = Current hot water temperature
+channel-type.remehaheating.outdoor-temperature.label = Outdoor Temperature
+channel-type.remehaheating.outdoor-temperature.description = Outdoor temperature
+channel-type.remehaheating.room-temperature.label = Room Temperature
+channel-type.remehaheating.room-temperature.description = Current room temperature
+channel-type.remehaheating.status.label = Status
+channel-type.remehaheating.status.description = Boiler status
+channel-type.remehaheating.target-temperature.label = Target Temperature
+channel-type.remehaheating.target-temperature.description = Target room temperature
+channel-type.remehaheating.water-pressure-ok.label = Water Pressure OK
+channel-type.remehaheating.water-pressure-ok.description = Water pressure status
+channel-type.remehaheating.water-pressure.label = Water Pressure
+channel-type.remehaheating.water-pressure.description = System water pressure
+
+# thing status descriptions
+
+offline.conf-error-no-credentials = Email and password are required
+offline.conf-error-invalid-config = Configuration error
+offline.comm-error-authentication-failed = Authentication failed
+offline.comm-error-authentication-error = Authentication error
+offline.comm-error-data-fetch-failed = Failed to retrieve data from API
+offline.comm-error-data-update-failed = Data update failed
diff --git a/bundles/org.openhab.binding.remehaheating/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.remehaheating/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 0000000000000..3e389971dc6ff
--- /dev/null
+++ b/bundles/org.openhab.binding.remehaheating/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,163 @@
+
+
+
+
+
+ Remeha Home heating system boiler
+ heating
+ Boiler
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ email
+
+ Remeha Home account email
+
+
+ password
+
+ Remeha Home account password
+
+
+
+ Interval to refresh data from Remeha API (from 30 to 3600 seconds)
+ 60
+
+
+
+
+
+ Number:Temperature
+
+ Current room temperature
+ temperature
+
+ Measurement
+ Temperature
+
+
+
+
+
+ Number:Temperature
+
+ Target room temperature
+ temperature
+
+ Setpoint
+ Temperature
+
+
+
+
+
+ Number:Temperature
+
+ Current hot water temperature
+ temperature
+
+ Measurement
+ Temperature
+
+
+
+
+
+ Number:Temperature
+
+ Target hot water temperature
+ temperature
+
+ Setpoint
+ Temperature
+
+
+
+
+
+ Number:Pressure
+
+ System water pressure
+ pressure
+
+ Measurement
+ Pressure
+
+
+
+
+
+ Number:Temperature
+
+ Outdoor temperature
+ temperature
+
+ Measurement
+ Temperature
+
+
+
+
+
+ String
+
+ Boiler status
+
+ Status
+
+
+
+
+
+ String
+
+ Domestic hot water zone mode
+
+ Control
+
+
+
+
+
+
+
+
+
+
+
+ String
+
+ Hot water status
+
+ Status
+
+
+
+
+
+ Switch
+
+ Water pressure status
+
+ Status
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.remehaheating/src/test/java/org/openhab/binding/remehaheating/internal/RemehaApiClientTest.java b/bundles/org.openhab.binding.remehaheating/src/test/java/org/openhab/binding/remehaheating/internal/RemehaApiClientTest.java
new file mode 100644
index 0000000000000..43c18a983130a
--- /dev/null
+++ b/bundles/org.openhab.binding.remehaheating/src/test/java/org/openhab/binding/remehaheating/internal/RemehaApiClientTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2010-2025 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.remehaheating.internal;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.openhab.binding.remehaheating.internal.api.RemehaApiClient;
+import org.openhab.core.io.net.http.HttpClientFactory;
+
+import com.google.gson.JsonObject;
+
+/**
+ * Unit tests for {@link RemehaApiClient}.
+ *
+ * @author Michael Fraedrich - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@NonNullByDefault
+public class RemehaApiClientTest {
+
+ private @Mock @NonNullByDefault({}) HttpClientFactory httpClientFactory;
+ private @Mock @NonNullByDefault({}) HttpClient httpClient;
+ private @Mock @NonNullByDefault({}) Request request;
+ private @Mock @NonNullByDefault({}) ContentResponse response;
+ private @NonNullByDefault({}) RemehaApiClient apiClient;
+
+ @BeforeEach
+ public void setUp() {
+ apiClient = new RemehaApiClient(httpClient);
+ }
+
+ @Test
+ public void testConstructor() {
+ assertNotNull(apiClient);
+ }
+
+ @Test
+ public void testGetDashboardWithoutToken() {
+ JsonObject result = apiClient.getDashboard();
+ assertNull(result);
+ }
+
+ @Test
+ public void testSetTemperature() {
+ boolean result = apiClient.setTemperature("zone123", 21.5);
+ assertFalse(result); // Should return false without access token
+ }
+
+ @Test
+ public void testSetDhwMode() {
+ boolean result = apiClient.setDhwMode("zone456", "schedule");
+ assertFalse(result); // Should return false without access token
+ }
+}
diff --git a/bundles/org.openhab.binding.remehaheating/src/test/java/org/openhab/binding/remehaheating/internal/RemehaHeatingBindingConstantsTest.java b/bundles/org.openhab.binding.remehaheating/src/test/java/org/openhab/binding/remehaheating/internal/RemehaHeatingBindingConstantsTest.java
new file mode 100644
index 0000000000000..059573da03267
--- /dev/null
+++ b/bundles/org.openhab.binding.remehaheating/src/test/java/org/openhab/binding/remehaheating/internal/RemehaHeatingBindingConstantsTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2010-2025 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.remehaheating.internal;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link RemehaHeatingBindingConstants}.
+ *
+ * @author Michael Fraedrich - Initial contribution
+ */
+@NonNullByDefault
+public class RemehaHeatingBindingConstantsTest {
+
+ @Test
+ public void testThingTypeUID() {
+ assertEquals("remehaheating", RemehaHeatingBindingConstants.THING_TYPE_BOILER.getBindingId());
+ assertEquals("boiler", RemehaHeatingBindingConstants.THING_TYPE_BOILER.getId());
+ }
+
+ @Test
+ public void testChannelConstants() {
+ assertEquals("room-temperature", RemehaHeatingBindingConstants.CHANNEL_ROOM_TEMPERATURE);
+ assertEquals("target-temperature", RemehaHeatingBindingConstants.CHANNEL_TARGET_TEMPERATURE);
+ assertEquals("dhw-temperature", RemehaHeatingBindingConstants.CHANNEL_DHW_TEMPERATURE);
+ assertEquals("dhw-target", RemehaHeatingBindingConstants.CHANNEL_DHW_TARGET);
+ assertEquals("water-pressure", RemehaHeatingBindingConstants.CHANNEL_WATER_PRESSURE);
+ assertEquals("outdoor-temperature", RemehaHeatingBindingConstants.CHANNEL_OUTDOOR_TEMPERATURE);
+ assertEquals("status", RemehaHeatingBindingConstants.CHANNEL_STATUS);
+ assertEquals("dhw-mode", RemehaHeatingBindingConstants.CHANNEL_DHW_MODE);
+ assertEquals("water-pressure-ok", RemehaHeatingBindingConstants.CHANNEL_WATER_PRESSURE_OK);
+ assertEquals("dhw-status", RemehaHeatingBindingConstants.CHANNEL_DHW_STATUS);
+ }
+
+ @Test
+ public void testConfigConstants() {
+ assertEquals("email", RemehaHeatingBindingConstants.CONFIG_EMAIL);
+ assertEquals("password", RemehaHeatingBindingConstants.CONFIG_PASSWORD);
+ assertEquals("refreshInterval", RemehaHeatingBindingConstants.CONFIG_REFRESH_INTERVAL);
+ }
+
+ @Test
+ public void testDhwModeConstants() {
+ assertEquals("anti-frost", RemehaHeatingBindingConstants.DHW_MODE_ANTI_FROST);
+ assertEquals("schedule", RemehaHeatingBindingConstants.DHW_MODE_SCHEDULE);
+ assertEquals("continuous-comfort", RemehaHeatingBindingConstants.DHW_MODE_CONTINUOUS_COMFORT);
+ }
+}
diff --git a/bundles/org.openhab.binding.remehaheating/src/test/java/org/openhab/binding/remehaheating/internal/RemehaHeatingConfigurationTest.java b/bundles/org.openhab.binding.remehaheating/src/test/java/org/openhab/binding/remehaheating/internal/RemehaHeatingConfigurationTest.java
new file mode 100644
index 0000000000000..2fd49188f73e6
--- /dev/null
+++ b/bundles/org.openhab.binding.remehaheating/src/test/java/org/openhab/binding/remehaheating/internal/RemehaHeatingConfigurationTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2010-2025 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.remehaheating.internal;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link RemehaHeatingConfiguration}.
+ *
+ * @author Michael Fraedrich - Initial contribution
+ */
+@NonNullByDefault
+public class RemehaHeatingConfigurationTest {
+
+ @Test
+ public void testDefaultValues() {
+ RemehaHeatingConfiguration config = new RemehaHeatingConfiguration();
+
+ assertEquals("", config.email);
+ assertEquals("", config.password);
+ assertEquals(60, config.refreshInterval);
+ }
+
+ @Test
+ public void testConfigurationValues() {
+ RemehaHeatingConfiguration config = new RemehaHeatingConfiguration();
+
+ config.email = "test@example.com";
+ config.password = "testpassword";
+ config.refreshInterval = 120;
+
+ assertEquals("test@example.com", config.email);
+ assertEquals("testpassword", config.password);
+ assertEquals(120, config.refreshInterval);
+ }
+}
diff --git a/bundles/org.openhab.binding.remehaheating/src/test/java/org/openhab/binding/remehaheating/internal/RemehaHeatingHandlerFactoryTest.java b/bundles/org.openhab.binding.remehaheating/src/test/java/org/openhab/binding/remehaheating/internal/RemehaHeatingHandlerFactoryTest.java
new file mode 100644
index 0000000000000..137c5bef4f7be
--- /dev/null
+++ b/bundles/org.openhab.binding.remehaheating/src/test/java/org/openhab/binding/remehaheating/internal/RemehaHeatingHandlerFactoryTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2010-2025 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.remehaheating.internal;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.ThingHandler;
+
+/**
+ * Unit tests for {@link RemehaHeatingHandlerFactory}.
+ *
+ * @author Michael Fraedrich - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@NonNullByDefault
+public class RemehaHeatingHandlerFactoryTest {
+
+ private @Mock @NonNullByDefault({}) Thing thing;
+ private @Mock @NonNullByDefault({}) HttpClientFactory httpClientFactory;
+ private @Mock @NonNullByDefault({}) org.eclipse.jetty.client.HttpClient httpClient;
+ private @NonNullByDefault({}) RemehaHeatingHandlerFactory factory;
+
+ @BeforeEach
+ public void setUp() {
+ lenient().when(httpClientFactory.createHttpClient("remehaheating")).thenReturn(httpClient);
+ factory = new RemehaHeatingHandlerFactory(httpClientFactory);
+ }
+
+ @Test
+ public void testSupportsThingType() {
+ assertTrue(factory.supportsThingType(RemehaHeatingBindingConstants.THING_TYPE_BOILER));
+ assertFalse(factory.supportsThingType(new ThingTypeUID("other", "thing")));
+ }
+
+ @Test
+ public void testCreateHandler() {
+ when(thing.getThingTypeUID()).thenReturn(RemehaHeatingBindingConstants.THING_TYPE_BOILER);
+
+ ThingHandler handler = factory.createHandler(thing);
+
+ assertNotNull(handler);
+ assertInstanceOf(RemehaHeatingHandler.class, handler);
+ verify(httpClient).setRequestBufferSize(16384);
+ verify(httpClient).setResponseBufferSize(16384);
+ }
+
+ @Test
+ public void testCreateHandlerForUnsupportedThing() {
+ when(thing.getThingTypeUID()).thenReturn(new ThingTypeUID("other", "thing"));
+
+ ThingHandler handler = factory.createHandler(thing);
+
+ assertNull(handler);
+ }
+}
diff --git a/bundles/org.openhab.binding.remehaheating/src/test/java/org/openhab/binding/remehaheating/internal/RemehaHeatingHandlerTest.java b/bundles/org.openhab.binding.remehaheating/src/test/java/org/openhab/binding/remehaheating/internal/RemehaHeatingHandlerTest.java
new file mode 100644
index 0000000000000..0e7ec2ab7b2f7
--- /dev/null
+++ b/bundles/org.openhab.binding.remehaheating/src/test/java/org/openhab/binding/remehaheating/internal/RemehaHeatingHandlerTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2010-2025 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.remehaheating.internal;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.lenient;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.StringType;
+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.thing.ThingUID;
+import org.openhab.core.thing.binding.ThingHandlerCallback;
+import org.openhab.core.types.RefreshType;
+
+/**
+ * Unit tests for {@link RemehaHeatingHandler}.
+ *
+ * @author Michael Fraedrich - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@NonNullByDefault
+public class RemehaHeatingHandlerTest {
+
+ private @Mock @NonNullByDefault({}) Thing thing;
+ private @Mock @NonNullByDefault({}) ThingHandlerCallback callback;
+ private @Mock @NonNullByDefault({}) Configuration configuration;
+ private @Mock @NonNullByDefault({}) org.eclipse.jetty.client.HttpClient httpClient;
+ private @NonNullByDefault({}) RemehaHeatingHandler handler;
+ private @NonNullByDefault({}) ThingUID thingUID;
+ private @NonNullByDefault({}) ChannelUID channelUID;
+
+ @BeforeEach
+ public void setUp() {
+ thingUID = new ThingUID(RemehaHeatingBindingConstants.THING_TYPE_BOILER, "test");
+ channelUID = new ChannelUID(thingUID, RemehaHeatingBindingConstants.CHANNEL_TARGET_TEMPERATURE);
+
+ lenient().when(thing.getUID()).thenReturn(thingUID);
+ lenient().when(thing.getConfiguration()).thenReturn(configuration);
+
+ handler = new RemehaHeatingHandler(thing, httpClient);
+ handler.setCallback(callback);
+ }
+
+ @Test
+ public void testConstructor() {
+ assertNotNull(handler);
+ }
+
+ @Test
+ public void testInitializeWithMissingCredentials() {
+ RemehaHeatingConfiguration config = new RemehaHeatingConfiguration();
+ config.email = "";
+ config.password = "";
+
+ when(configuration.as(RemehaHeatingConfiguration.class)).thenReturn(config);
+
+ handler.initialize();
+
+ verify(callback).statusUpdated(eq(thing), argThat(status -> status.getStatus() == ThingStatus.OFFLINE
+ && status.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR));
+ }
+
+ @Test
+ public void testHandleRefreshCommand() {
+ ChannelUID channelUID = new ChannelUID(thingUID, RemehaHeatingBindingConstants.CHANNEL_ROOM_TEMPERATURE);
+
+ // Should not throw exception
+ assertDoesNotThrow(() -> handler.handleCommand(channelUID, RefreshType.REFRESH));
+ }
+
+ @Test
+ public void testHandleTargetTemperatureCommand() {
+ DecimalType temperature = new DecimalType(21.5);
+
+ // Should not throw exception
+ assertDoesNotThrow(() -> handler.handleCommand(channelUID, temperature));
+ }
+
+ @Test
+ public void testHandleDhwModeCommand() {
+ ChannelUID dhwChannelUID = new ChannelUID(thingUID, RemehaHeatingBindingConstants.CHANNEL_DHW_MODE);
+ StringType mode = new StringType("schedule");
+
+ // Should not throw exception
+ assertDoesNotThrow(() -> handler.handleCommand(dhwChannelUID, mode));
+ }
+
+ @Test
+ public void testDispose() {
+ // Should not throw exception
+ assertDoesNotThrow(() -> handler.dispose());
+ }
+}
diff --git a/bundles/org.openhab.binding.remehaheating/src/test/resources/dashboard-sample.json b/bundles/org.openhab.binding.remehaheating/src/test/resources/dashboard-sample.json
new file mode 100644
index 0000000000000..e7b290d65dece
--- /dev/null
+++ b/bundles/org.openhab.binding.remehaheating/src/test/resources/dashboard-sample.json
@@ -0,0 +1,28 @@
+{
+ "appliances": [
+ {
+ "waterPressure": 1.5,
+ "errorStatus": "OK",
+ "waterPressureOK": true,
+ "outdoorTemperatureInformation": {
+ "internetOutdoorTemperature": 15.2
+ },
+ "climateZones": [
+ {
+ "climateZoneId": "zone123",
+ "roomTemperature": 20.5,
+ "setPoint": 21.0
+ }
+ ],
+ "hotWaterZones": [
+ {
+ "hotWaterZoneId": "zone456",
+ "dhwTemperature": 45.0,
+ "targetSetpoint": 50.0,
+ "dhwZoneMode": "schedule",
+ "dhwStatus": "heating"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/bundles/pom.xml b/bundles/pom.xml
index d587a5f67ad0b..f94d8b2a334e9 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -361,6 +361,7 @@
org.openhab.binding.radiobrowser
org.openhab.binding.radiothermostat
org.openhab.binding.regoheatpump
+ org.openhab.binding.remehaheating
org.openhab.binding.revogi
org.openhab.binding.remoteopenhab
org.openhab.binding.renault