mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-29 12:34:21 +02:00
[remehaheating] Initial contribution (#19494)
* [remehaheating] Initial contribution Signed-off-by: Michael Fraedrich <it-services@freddy4u.de>
This commit is contained in:
@@ -331,6 +331,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
|
||||
|
||||
@@ -1636,6 +1636,11 @@
|
||||
<artifactId>org.openhab.binding.regoheatpump</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.remehaheating</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.remoteopenhab</artifactId>
|
||||
|
||||
@@ -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
|
||||
@@ -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="<your-email@example.com>",
|
||||
password="<your-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
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>5.2.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.remehaheating</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: RemehaHeating Binding</name>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.remehaheating-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-remehaheating" description="RemehaHeating Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.remehaheating/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
+75
@@ -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";
|
||||
}
|
||||
+46
@@ -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;
|
||||
}
|
||||
+349
@@ -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;
|
||||
}
|
||||
}
|
||||
+91
@@ -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<ThingTypeUID> 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;
|
||||
}
|
||||
}
|
||||
+366
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<addon:addon id="remehaheating" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
|
||||
|
||||
<type>binding</type>
|
||||
<name>Remeha Heating Binding</name>
|
||||
<description>This binding integrates Remeha Home heating systems, allowing control and monitoring of boilers through
|
||||
the Remeha Home cloud service.</description>
|
||||
<connection>cloud</connection>
|
||||
|
||||
</addon:addon>
|
||||
+53
@@ -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
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="remehaheating"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="boiler">
|
||||
<label>Remeha Boiler</label>
|
||||
<description>Remeha Home heating system boiler</description>
|
||||
<category>heating</category>
|
||||
<semantic-equipment-tag>Boiler</semantic-equipment-tag>
|
||||
|
||||
<channels>
|
||||
<channel id="room-temperature" typeId="room-temperature"/>
|
||||
<channel id="target-temperature" typeId="target-temperature"/>
|
||||
<channel id="dhw-temperature" typeId="dhw-temperature"/>
|
||||
<channel id="dhw-target" typeId="dhw-target"/>
|
||||
<channel id="dhw-mode" typeId="dhw-mode"/>
|
||||
<channel id="dhw-status" typeId="dhw-status"/>
|
||||
<channel id="water-pressure" typeId="water-pressure"/>
|
||||
<channel id="water-pressure-ok" typeId="water-pressure-ok"/>
|
||||
<channel id="outdoor-temperature" typeId="outdoor-temperature"/>
|
||||
<channel id="status" typeId="status"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="email" type="text" required="true">
|
||||
<context>email</context>
|
||||
<label>Email</label>
|
||||
<description>Remeha Home account email</description>
|
||||
</parameter>
|
||||
<parameter name="password" type="text" required="true">
|
||||
<context>password</context>
|
||||
<label>Password</label>
|
||||
<description>Remeha Home account password</description>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer" min="30" max="3600" unit="s">
|
||||
<label>Refresh Interval</label>
|
||||
<description>Interval to refresh data from Remeha API (from 30 to 3600 seconds)</description>
|
||||
<default>60</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="room-temperature">
|
||||
<item-type unitHint="°C">Number:Temperature</item-type>
|
||||
<label>Room Temperature</label>
|
||||
<description>Current room temperature</description>
|
||||
<category>temperature</category>
|
||||
<tags>
|
||||
<tag>Measurement</tag>
|
||||
<tag>Temperature</tag>
|
||||
</tags>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="target-temperature">
|
||||
<item-type unitHint="°C">Number:Temperature</item-type>
|
||||
<label>Target Temperature</label>
|
||||
<description>Target room temperature</description>
|
||||
<category>temperature</category>
|
||||
<tags>
|
||||
<tag>Setpoint</tag>
|
||||
<tag>Temperature</tag>
|
||||
</tags>
|
||||
<state min="5" max="30" step="0.5" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="dhw-temperature">
|
||||
<item-type unitHint="°C">Number:Temperature</item-type>
|
||||
<label>Hot Water Temperature</label>
|
||||
<description>Current hot water temperature</description>
|
||||
<category>temperature</category>
|
||||
<tags>
|
||||
<tag>Measurement</tag>
|
||||
<tag>Temperature</tag>
|
||||
</tags>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="dhw-target">
|
||||
<item-type unitHint="°C">Number:Temperature</item-type>
|
||||
<label>Hot Water Target</label>
|
||||
<description>Target hot water temperature</description>
|
||||
<category>temperature</category>
|
||||
<tags>
|
||||
<tag>Setpoint</tag>
|
||||
<tag>Temperature</tag>
|
||||
</tags>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="water-pressure">
|
||||
<item-type unitHint="bar">Number:Pressure</item-type>
|
||||
<label>Water Pressure</label>
|
||||
<description>System water pressure</description>
|
||||
<category>pressure</category>
|
||||
<tags>
|
||||
<tag>Measurement</tag>
|
||||
<tag>Pressure</tag>
|
||||
</tags>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="outdoor-temperature">
|
||||
<item-type unitHint="°C">Number:Temperature</item-type>
|
||||
<label>Outdoor Temperature</label>
|
||||
<description>Outdoor temperature</description>
|
||||
<category>temperature</category>
|
||||
<tags>
|
||||
<tag>Measurement</tag>
|
||||
<tag>Temperature</tag>
|
||||
</tags>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="status">
|
||||
<item-type>String</item-type>
|
||||
<label>Status</label>
|
||||
<description>Boiler status</description>
|
||||
<tags>
|
||||
<tag>Status</tag>
|
||||
</tags>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="dhw-mode">
|
||||
<item-type>String</item-type>
|
||||
<label>Hot Water Mode</label>
|
||||
<description>Domestic hot water zone mode</description>
|
||||
<tags>
|
||||
<tag>Control</tag>
|
||||
</tags>
|
||||
<state>
|
||||
<options>
|
||||
<option value="anti-frost">Anti-frost</option>
|
||||
<option value="schedule">Schedule</option>
|
||||
<option value="continuous-comfort">Continuous Comfort</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="dhw-status">
|
||||
<item-type>String</item-type>
|
||||
<label>Hot Water Status</label>
|
||||
<description>Hot water status</description>
|
||||
<tags>
|
||||
<tag>Status</tag>
|
||||
</tags>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="water-pressure-ok">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Water Pressure OK</label>
|
||||
<description>Water pressure status</description>
|
||||
<tags>
|
||||
<tag>Status</tag>
|
||||
</tags>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
+75
@@ -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
|
||||
}
|
||||
}
|
||||
+61
@@ -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);
|
||||
}
|
||||
}
|
||||
+49
@@ -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);
|
||||
}
|
||||
}
|
||||
+75
@@ -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);
|
||||
}
|
||||
}
|
||||
+115
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -364,6 +364,7 @@
|
||||
<module>org.openhab.binding.radiobrowser</module>
|
||||
<module>org.openhab.binding.radiothermostat</module>
|
||||
<module>org.openhab.binding.regoheatpump</module>
|
||||
<module>org.openhab.binding.remehaheating</module>
|
||||
<module>org.openhab.binding.revogi</module>
|
||||
<module>org.openhab.binding.remoteopenhab</module>
|
||||
<module>org.openhab.binding.renault</module>
|
||||
|
||||
Reference in New Issue
Block a user