diff --git a/bundles/org.openhab.binding.ondilo/README.md b/bundles/org.openhab.binding.ondilo/README.md
index 506456fe65..518326a736 100644
--- a/bundles/org.openhab.binding.ondilo/README.md
+++ b/bundles/org.openhab.binding.ondilo/README.md
@@ -4,8 +4,8 @@ This binding integrates Ondilo ICO pool monitoring devices with openHAB, allowin
## Supported Things
-`account:` Represents your Ondilo Account (authentication using OAuth2 flow)
-`ondilo:` Represents an individual Ondilo ICO device
+- `account:` Represents your Ondilo Account (authentication using OAuth2 flow)
+- `ondilo:` Represents an individual Ondilo ICO device
Ondilo ICO Pool as well as Spa devices are supported.
Chlor as well as salt water.
@@ -20,7 +20,7 @@ Each Ondilo ICO will appear as a new Thing in the inbox.
### `account` Thing Configuration
- **url**: The URL of the openHAB instance. Required for the redirect during OAuth2 authentication flow (e.g. `http://localhost:8080`)
-- **refreshInterval**: Polling interval in seconds (default: `900 s`).
+- **refreshInterval**: Polling interval in seconds (default: `900 s`)
### `ondilo` Thing Configuration
@@ -35,8 +35,10 @@ The requests to the Ondilo Customer API are limited to the following per user qu
- 5 requests per second
- 30 requests per hour
-`account` Thing performs 1 request per cycle - 4 per hour per Ondilo Account with default interval.
-`ondilo` Thing performs 2 requests per cycle - 8 per hour per Ondilo ICO with default interval.
+Example using default interval:
+
+- `account` Thing performs 2 request per cycle - 8 requests per hour per Ondilo Account
+- `ondilo` Thing performs 4 requests per cycle - 16 requests per hour per Ondilo ICO
## Channels
@@ -48,16 +50,21 @@ The requests to the Ondilo Customer API are limited to the following per user qu
### Measures Channels
-| Channel ID | Type | Advanced | Access | Description |
-|---------------------------|-------------------------|----------|--------|--------------------------------------------------------|
-| temperature | Number:Temperature | false | R | Water temperature in the pool |
-| ph | Number | false | R | pH value of the pool water |
-| orp | Number:ElectricPotential| false | R | Oxidation-reduction potential (ORP) |
-| salt | Number:Density | false | R | Salt concentration in the pool (salt pools only) |
-| tds | Number:Density | false | R | Total dissolved solids in the pool (chlor pools only ) |
-| battery | Number:Dimensionless | false | R | Battery level of the device |
-| rssi | Number:Dimensionless | false | R | Signal strength (RSSI) |
-| value-time | DateTime | true | R | Timestamp of the set of measures |
+| Channel ID | Type | Advanced | Access | Description |
+|---------------------------|--------------------------|----------|--------|--------------------------------------------------------|
+| temperature | Number:Temperature | false | R | Water temperature in the pool |
+| temperature-trend | Number:Temperature | true | R | Change in water temperature since last measure |
+| ph | Number | false | R | pH value of the pool water |
+| ph-trend | Number | true | R | Change in pH value since last measure |
+| orp | Number:ElectricPotential | false | R | Oxidation-reduction potential (ORP) |
+| orp-trend | Number:ElectricPotential | true | R | Change in ORP since last measure |
+| salt | Number:Density | false | R | Salt concentration in the pool (salt pools only) |
+| salt-trend | Number:Density | true | R | Change in salt concentration since last measure |
+| tds | Number:Density | false | R | Total dissolved solids in the pool (chlor pools only) |
+| tds-trend | Number:Density | true | R | Change in TDS since last measure |
+| battery | Number:Dimensionless | false | R | Battery level of the device |
+| rssi | Number:Dimensionless | false | R | Signal strength (RSSI) |
+| value-time | DateTime | true | R | Timestamp of the set of measures |
### Recommendations Channels
@@ -71,13 +78,28 @@ The requests to the Ondilo Customer API are limited to the following per user qu
| recommendation-status | String | false | R/W | Status of the current recommendation (`waiting`/`ok`)
`sendCommand("ok")` to validate current `waiting` recommendation |
| recommendation-deadline | String | true | R | Deadline of the current recommendation |
+### Configuration Channels
+
+| Channel ID | Type | Advanced | Access | Description |
+|---------------------------|----------------|----------|--------|-----------------------------------------------|
+| temperature-low | Number | true | R | Minimum water temperature |
+| temperature-high | Number | true | R | Maximum water temperature |
+| ph-low | Number | true | R | Minimum pH value |
+| ph-high | Number | true | R | Maximum pH value |
+| orp-low | Number | true | R | Minimum ORP value |
+| orp-high | Number | true | R | Maximum ORP value |
+| salt-low | Number | true | R | Minimum salt concentration (salt pools only) |
+| salt-high | Number | true | R | Maximum salt concentration (salt pools only) |
+| tds-low | Number | true | R | Minimum TDS value (chlor pools only) |
+| tds-high | Number | true | R | Maximum TDS value (chlor pools only) |
+
## Full Example
### Thing Configuration
```Java
Bridge ondilo:account:ondiloAccount [ url="http://localhost:8080", refreshInterval=900 ] {
- Thing ondilo "" [ id="" ] {
+ Thing ondilo 12345 [ id=12345 ] { // 12345 is an example of the id received via discovery
}
```
@@ -124,3 +146,4 @@ sitemap demo label="Ondilo ICO" {
- [Ondilo API Documentation](https://interop.ondilo.com/docs/api/customer/v1)
- [openHAB Community Forum](https://community.openhab.org/t/request-ondilo-binding/98164)
+- [Pool Monitor Card Widget](https://community.openhab.org/t/pool-monitor-card/164693)
diff --git a/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloApiClient.java b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloApiClient.java
index 1e093e1ec3..53f01ec3dc 100644
--- a/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloApiClient.java
+++ b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloApiClient.java
@@ -31,6 +31,8 @@ import org.openhab.core.auth.client.oauth2.OAuthResponseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.google.gson.Gson;
+
/**
* The {@link OndiloApiClient} for accessing the Ondilo API using OAuth2 authentication.
* handlers.
@@ -44,6 +46,12 @@ public class OndiloApiClient {
private @Nullable String bearer;
private @Nullable AccessTokenResponse accessTokenResponse;
private static final String ONDILO_API_URL = "https://interop.ondilo.com/api/customer/v1";
+ private static final Gson GSON = new Gson();
+
+ private static long lastRequestTime = 0;
+ // Minimum interval between requests in milliseconds
+ // Enforce API rate limit: 5 requests max per 1 second
+ private static final long MIN_REQUEST_INTERVAL_MS = 200;
public OndiloApiClient(OAuthClientService oAuthService, AccessTokenResponse accessTokenResponse) {
this.oAuthService = oAuthService;
@@ -53,50 +61,36 @@ public class OndiloApiClient {
}
@Nullable
- public synchronized String get(String endpoint) {
- try {
- refreshAccessTokenIfNeeded();
- URL url = new URI(ONDILO_API_URL + endpoint).toURL();
- HttpURLConnection conn = (HttpURLConnection) url.openConnection();
- conn.setRequestMethod("GET");
- conn.setRequestProperty("Authorization", "Bearer " + bearer);
- conn.setRequestProperty("Accept", "application/json");
- conn.setRequestProperty("Accept-Charset", "utf-8");
- conn.setRequestProperty("Accept-Encoding", "gzip, deflate");
- conn.connect();
- int responseCode = conn.getResponseCode();
- if (responseCode == 200) {
- try (InputStream is = conn.getInputStream(); Scanner scanner = new Scanner(is, "UTF-8")) {
- return scanner.useDelimiter("\\A").next();
- }
- } else {
- logger.warn("Ondilo API request failed with code: {}", responseCode);
+ public synchronized T request(String requestMethod, String endpoint, Class type) {
+ // Enforce API rate limit: at least 200ms between requests
+ long now = System.currentTimeMillis();
+ long wait = lastRequestTime + MIN_REQUEST_INTERVAL_MS - now;
+ if (wait > 0) {
+ try {
+ Thread.sleep(wait);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ logger.debug("API request pause interrupted: {}", e.getMessage());
+ return null;
}
- } catch (InterruptedIOException e) {
- logger.debug("Ondilo API request interrupted: {}", e.getMessage());
- Thread.currentThread().interrupt();
- } catch (IOException | URISyntaxException e) {
- logger.warn("Ondilo API request error", e);
}
- return null;
- }
-
- @Nullable
- public synchronized String put(String endpoint) {
try {
refreshAccessTokenIfNeeded();
URL url = new URI(ONDILO_API_URL + endpoint).toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
- conn.setRequestMethod("PUT");
+ conn.setRequestMethod(requestMethod);
conn.setRequestProperty("Authorization", "Bearer " + bearer);
conn.setRequestProperty("Accept", "application/json");
conn.setRequestProperty("Accept-Charset", "utf-8");
conn.setRequestProperty("Accept-Encoding", "gzip, deflate");
conn.connect();
+ lastRequestTime = System.currentTimeMillis();
int responseCode = conn.getResponseCode();
if (responseCode == 200) {
try (InputStream is = conn.getInputStream(); Scanner scanner = new Scanner(is, "UTF-8")) {
- return scanner.useDelimiter("\\A").next();
+ String response = scanner.useDelimiter("\\A").next();
+ // Parse JSON to DTO
+ return GSON.fromJson(response, type);
}
} else {
logger.warn("Ondilo API request failed with code: {}", responseCode);
diff --git a/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloBindingConstants.java b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloBindingConstants.java
index b12739cf5d..dadf25e341 100644
--- a/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloBindingConstants.java
+++ b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloBindingConstants.java
@@ -30,26 +30,37 @@ public class OndiloBindingConstants {
public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "account");
public static final ThingTypeUID THING_TYPE_ONDILO = new ThingTypeUID(BINDING_ID, "ondilo");
+ // Bridge Properties
+ public static final String PROPERTY_BRIDGE_USER_INFO = "userInfo";
+
// Bridge Channel ids
public static final String CHANNEL_POLL_UPDATE = "poll-update";
// Ondilo Thing Properties
- public static final String ONDILO_ID = "id";
- public static final String ONDILO_NAME = "name";
- public static final String ONDILO_TYPE = "type";
- public static final String ONDILO_VOLUME = "volume";
- public static final String ONDILO_DISINFECTION = "disinfection";
- public static final String ONDILO_ADDRESS = "address";
- public static final String ONDILO_LOCATION = "location";
+ public static final String PROPERTY_ONDILO_ID = "id";
+ public static final String PROPERTY_ONDILO_NAME = "name";
+ public static final String PROPERTY_ONDILO_TYPE = "type";
+ public static final String PROPERTY_ONDILO_VOLUME = "volume";
+ public static final String PROPERTY_ONDILO_DISINFECTION = "disinfection";
+ public static final String PROPERTY_ONDILO_ADDRESS = "address";
+ public static final String PROPERTY_ONDILO_LOCATION = "location";
+ public static final String PROPERTY_ONDILO_INFO_UUID = "uuid";
+ public static final String PROPERTY_ONDILO_INFO_POOL_GUY_NUMBER = "poolGuyNumber";
+ public static final String PROPERTY_ONDILO_INFO_MAINTENANCE_DAY = "maintenanceDay";
// Ondilo Thing Measures Channel ids
public static final String GROUP_MEASURES = "measure#";
public static final String CHANNEL_TEMPERATURE = GROUP_MEASURES + "temperature";
+ public static final String CHANNEL_TEMPERATURE_TREND = GROUP_MEASURES + "temperature-trend";
public static final String CHANNEL_PH = GROUP_MEASURES + "ph";
+ public static final String CHANNEL_PH_TREND = GROUP_MEASURES + "ph-trend";
public static final String CHANNEL_ORP = GROUP_MEASURES + "orp";
+ public static final String CHANNEL_ORP_TREND = GROUP_MEASURES + "orp-trend";
public static final String CHANNEL_SALT = GROUP_MEASURES + "salt";
+ public static final String CHANNEL_SALT_TREND = GROUP_MEASURES + "salt-trend";
public static final String CHANNEL_TDS = GROUP_MEASURES + "tds";
+ public static final String CHANNEL_TDS_TREND = GROUP_MEASURES + "tds-trend";
public static final String CHANNEL_BATTERY = GROUP_MEASURES + "battery";
public static final String CHANNEL_RSSI = GROUP_MEASURES + "rssi";
public static final String CHANNEL_VALUE_TIME = GROUP_MEASURES + "value-time";
@@ -65,6 +76,20 @@ public class OndiloBindingConstants {
public static final String CHANNEL_RECOMMENDATION_STATUS = GROUP_RECOMMENDATIONS + "status";
public static final String CHANNEL_RECOMMENDATION_DEADLINE = GROUP_RECOMMENDATIONS + "deadline";
+ // Ondilo Thing Configuration Channel ids
+ public static final String GROUP_CONFIGURATION = "configuration#";
+
+ public static final String CHANNEL_CONFIGURATION_TEMPERATURE_LOW = GROUP_CONFIGURATION + "temperature-low";
+ public static final String CHANNEL_CONFIGURATION_TEMPERATURE_HIGH = GROUP_CONFIGURATION + "temperature-high";
+ public static final String CHANNEL_CONFIGURATION_PH_LOW = GROUP_CONFIGURATION + "ph-low";
+ public static final String CHANNEL_CONFIGURATION_PH_HIGH = GROUP_CONFIGURATION + "ph-high";
+ public static final String CHANNEL_CONFIGURATION_ORP_LOW = GROUP_CONFIGURATION + "orp-low";
+ public static final String CHANNEL_CONFIGURATION_ORP_HIGH = GROUP_CONFIGURATION + "orp-high";
+ public static final String CHANNEL_CONFIGURATION_SALT_LOW = GROUP_CONFIGURATION + "salt-low";
+ public static final String CHANNEL_CONFIGURATION_SALT_HIGH = GROUP_CONFIGURATION + "salt-high";
+ public static final String CHANNEL_CONFIGURATION_TDS_LOW = GROUP_CONFIGURATION + "tds-low";
+ public static final String CHANNEL_CONFIGURATION_TDS_HIGH = GROUP_CONFIGURATION + "tds-high";
+
// I18N keys for state details
public static final String I18N_URL_INVALID = "@text/thing.ondilo.bridge.config.url.invalid";
public static final String I18N_OAUTH2_PENDING = "@text/thing.ondilo.bridge.config.oauth2.pending";
diff --git a/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloBridge.java b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloBridge.java
index 5d51d1a255..617fadfd0e 100644
--- a/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloBridge.java
+++ b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloBridge.java
@@ -14,7 +14,7 @@ package org.openhab.binding.ondilo.internal;
import java.time.Duration;
import java.time.Instant;
-import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -26,15 +26,15 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ondilo.internal.dto.LastMeasure;
import org.openhab.binding.ondilo.internal.dto.Pool;
+import org.openhab.binding.ondilo.internal.dto.PoolConfiguration;
+import org.openhab.binding.ondilo.internal.dto.PoolInfo;
import org.openhab.binding.ondilo.internal.dto.Recommendation;
+import org.openhab.binding.ondilo.internal.dto.UserInfo;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import com.google.gson.Gson;
-import com.google.gson.reflect.TypeToken;
-
/**
* The {@link OndiloBridge} handles OAuth2 authentication for Ondilo API.
*
@@ -45,6 +45,7 @@ public class OndiloBridge {
private final Logger logger = LoggerFactory.getLogger(OndiloBridge.class);
private final ScheduledExecutorService scheduler;
private final int refreshInterval;
+ private final OndiloBridgeHandler bridgeHandler;
private @Nullable ScheduledFuture> ondiloBridgePollingJob;
private @Nullable List pools;
public @Nullable OndiloApiClient apiClient;
@@ -54,10 +55,11 @@ public class OndiloBridge {
// last measure. For whatever reason it takes a bit longer for the measures to finally reach the cloud and be
// available via API. Therefore we add a buffer of 4 minutes to the next polling time.
// If the last measure was taken at 12:00, we will poll again at 13:04.
- private static final int DEFAULT_REFRESH_INTERVAL = 64;
+ private static final Duration TARGET_REFRESH_INTERVAL = Duration.ofMinutes(60 + 4);
public OndiloBridge(OndiloBridgeHandler bridgeHandler, OAuthClientService oAuthService,
AccessTokenResponse accessTokenResponse, int refreshInterval, ScheduledExecutorService scheduler) {
+ this.bridgeHandler = bridgeHandler;
this.scheduler = scheduler;
this.refreshInterval = refreshInterval;
@@ -81,7 +83,7 @@ public class OndiloBridge {
private void startOndiloBridgePolling(Integer refreshInterval) {
if (ondiloBridgePollingJob == null) {
- ondiloBridgePollingJob = scheduler.scheduleWithFixedDelay(() -> pollOndiloICOs(), 1, refreshInterval,
+ ondiloBridgePollingJob = scheduler.scheduleWithFixedDelay(() -> pollOndiloICOs(), 10, refreshInterval,
TimeUnit.SECONDS);
} else {
logger.warn("Ondilo bridge polling job is already running, not starting a new one");
@@ -100,28 +102,20 @@ public class OndiloBridge {
try {
OndiloApiClient apiClient = this.apiClient;
if (apiClient != null) {
- String poolsJson = apiClient.get("/pools");
- logger.trace("Ondilo ICOs: {}", poolsJson);
- // Parse JSON to DTO
- Gson gson = new Gson();
- List pools = gson.fromJson(poolsJson, new TypeToken>() {
- }.getType());
- if (pools != null && !pools.isEmpty()) {
- logger.trace("Polled {} Ondilo ICOs", pools.size());
+ UserInfo userInfo = apiClient.request("GET", "/user/info", UserInfo.class);
+ if (userInfo != null) {
+ bridgeHandler.updateUserInfo(userInfo);
+ }
+
+ Pool pools[] = apiClient.request("GET", "/pools", Pool[].class);
+ if (pools != null && !(pools.length == 0)) {
+ logger.trace("Polled {} Ondilo ICOs", pools.length);
// Poll last measures and recommendations for each pool
Instant lastValueTime = null;
for (Pool pool : pools) {
- try {
- // Pause for 1 second between polls in order to keep API rate limits
- Thread.sleep(1000);
- Instant valueTime = pollOndiloICO(pool.id);
- if (lastValueTime == null || (valueTime != null && valueTime.isBefore(lastValueTime))) {
- lastValueTime = valueTime;
- }
- } catch (InterruptedException ie) {
- Thread.currentThread().interrupt();
- logger.warn("Polling pause interrupted: {}", ie.getMessage());
- break;
+ Instant valueTime = pollOndiloICO(pool.id);
+ if (lastValueTime == null || (valueTime != null && valueTime.isBefore(lastValueTime))) {
+ lastValueTime = valueTime;
}
}
if (lastValueTime != null) {
@@ -130,7 +124,8 @@ public class OndiloBridge {
} else {
logger.warn("No Ondilo ICO found or failed to parse JSON response");
}
- this.pools = pools;
+ // Store the pools for later access
+ this.pools = Arrays.asList(pools);
} else {
logger.warn("OndiloApiClient is not initialized, cannot poll Ondilo ICOs");
}
@@ -142,7 +137,7 @@ public class OndiloBridge {
private void adaptPollingToValueTime(Instant lastValueTime, int refreshInterval) {
// Adjusting the polling reduces the delay when new measures get available, without polling too frequently and
// hitting API rate limits.
- Instant nextValueTime = lastValueTime.plus(DEFAULT_REFRESH_INTERVAL, ChronoUnit.MINUTES);
+ Instant nextValueTime = lastValueTime.plus(TARGET_REFRESH_INTERVAL);
Instant now = Instant.now();
Instant scheduledTime = now.plusSeconds(refreshInterval);
if (nextValueTime.isBefore(scheduledTime)) {
@@ -164,15 +159,11 @@ public class OndiloBridge {
OndiloApiClient apiClient = this.apiClient;
Instant lastValueTime = null;
if (ondiloHandler != null && apiClient != null) {
- String poolsJson = apiClient.get("/pools/" + id
- + "/lastmeasures?types[]=temperature&types[]=ph&types[]=orp&types[]=salt&types[]=tds&types[]=battery&types[]=rssi");
- logger.trace("LastMeasures: {}", poolsJson);
- // Parse JSON to DTO
- Gson gson = new Gson();
- List lastMeasures = gson.fromJson(poolsJson, new TypeToken>() {
- }.getType());
+ LastMeasure[] lastMeasures = apiClient.request("GET", "/pools/" + id
+ + "/lastmeasures?types[]=temperature&types[]=ph&types[]=orp&types[]=salt&types[]=tds&types[]=battery&types[]=rssi",
+ LastMeasure[].class);
- if (lastMeasures == null || lastMeasures.isEmpty()) {
+ if (lastMeasures == null || lastMeasures.length == 0) {
logger.warn("No lastMeasures available for Ondilo ICO with ID: {}", id);
ondiloHandler.clearLastMeasuresChannels();
} else {
@@ -185,14 +176,10 @@ public class OndiloBridge {
}
}
- String recommendationsJson = apiClient.get("/pools/" + id + "/recommendations");
- logger.trace("recommendations: {}", recommendationsJson);
- // Parse JSON to DTO
- List recommendations = gson.fromJson(recommendationsJson,
- new TypeToken>() {
- }.getType());
+ Recommendation[] recommendations = apiClient.request("GET", "/pools/" + id + "/recommendations",
+ Recommendation[].class);
- if (recommendations == null || recommendations.isEmpty()) {
+ if (recommendations == null || recommendations.length == 0) {
logger.trace("No Recommendations available for Ondilo ICO with ID: {}", id);
ondiloHandler.clearRecommendationChannels();
} else {
@@ -209,11 +196,23 @@ public class OndiloBridge {
waitingRecommendation.title);
recommendation = waitingRecommendation;
} else {
- recommendation = recommendations.get(0);
+ recommendation = recommendations[0];
logger.trace("Latest Recommentation: id={}, title={}", recommendation.id, recommendation.title);
}
ondiloHandler.updateRecommendationChannels(recommendation);
}
+
+ PoolInfo poolInfo = apiClient.request("GET", "/pools/" + id + "/device", PoolInfo.class);
+ if (poolInfo != null) {
+ ondiloHandler.updatePoolInfo(poolInfo);
+ }
+
+ PoolConfiguration poolConfiguration = apiClient.request("GET", "/pools/" + id + "/configuration",
+ PoolConfiguration.class);
+ if (poolConfiguration != null) {
+ ondiloHandler.updatePoolConfiguration(poolConfiguration);
+ ondiloHandler.updatePoolInfo(poolConfiguration);
+ }
} else {
logger.debug("No OndiloHandler found for Ondilo ICO with ID: {}", id);
}
@@ -223,8 +222,9 @@ public class OndiloBridge {
public void validateRecommendation(int poolId, int recommendationId) {
OndiloApiClient apiClient = this.apiClient;
if (apiClient != null) {
- String response = apiClient.put("/pools/" + poolId + "/recommendations/" + recommendationId);
- if (response != null && "\"Done\"".equals(response.trim())) {
+ String response = apiClient.request("PUT", "/pools/" + poolId + "/recommendations/" + recommendationId,
+ String.class);
+ if (response != null && "Done".equals(response.trim())) {
logger.trace("Validated Recommendation successfully");
} else {
logger.warn("Failed to validate Recommendation, API response: {}", response);
diff --git a/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloBridgeHandler.java b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloBridgeHandler.java
index 6f58847e7a..fa35b80f89 100644
--- a/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloBridgeHandler.java
+++ b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloBridgeHandler.java
@@ -19,12 +19,14 @@ import java.io.InterruptedIOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ondilo.internal.dto.Pool;
+import org.openhab.binding.ondilo.internal.dto.UserInfo;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthException;
@@ -189,6 +191,12 @@ public class OndiloBridgeHandler extends BaseBridgeHandler {
}
}
+ public void updateUserInfo(UserInfo userInfo) {
+ Map properties = editProperties();
+ properties.put(PROPERTY_BRIDGE_USER_INFO, userInfo.getUserInfo());
+ updateProperties(properties);
+ }
+
@Override
public void dispose() {
String clientSecret = thing.getUID().getAsString();
diff --git a/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloHandler.java b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloHandler.java
index d325f1001c..7165400ce7 100644
--- a/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloHandler.java
+++ b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloHandler.java
@@ -17,14 +17,21 @@ import static org.openhab.binding.ondilo.internal.OndiloBindingConstants.*;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
+import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
+import javax.measure.Unit;
+
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ondilo.internal.dto.LastMeasure;
+import org.openhab.binding.ondilo.internal.dto.Pool;
+import org.openhab.binding.ondilo.internal.dto.PoolConfiguration;
+import org.openhab.binding.ondilo.internal.dto.PoolInfo;
import org.openhab.binding.ondilo.internal.dto.Recommendation;
+import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
@@ -52,15 +59,28 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault
public class OndiloHandler extends BaseThingHandler {
private static final String NO_ID = "NO_ID";
+
private final Logger logger = LoggerFactory.getLogger(OndiloHandler.class);
+ private final LocaleProvider localeProvider;
+ private final int configPoolId;
+
private int recommendationId; // Used to track the last recommendation ID processed
private AtomicReference ondiloId = new AtomicReference<>(NO_ID);
- private final int configPoolId;
+
private @Nullable ScheduledFuture> bridgeRecoveryJob;
- public OndiloHandler(Thing thing) {
+ // Store last value and valueTime for trend calculation
+ private OndiloMeasureState lastTemperatureState = new OndiloMeasureState(Double.NaN, null);
+ private OndiloMeasureState lastPhState = new OndiloMeasureState(Double.NaN, null);
+ private OndiloMeasureState lastOrpState = new OndiloMeasureState(Double.NaN, null);
+ private OndiloMeasureState lastSaltState = new OndiloMeasureState(Double.NaN, null);
+ private OndiloMeasureState lastTdsState = new OndiloMeasureState(Double.NaN, null);
+
+ public OndiloHandler(Thing thing, LocaleProvider localeProvider) {
super(thing);
+ this.localeProvider = localeProvider;
+
OndiloConfiguration currentConfig = getConfigAs(OndiloConfiguration.class);
this.configPoolId = currentConfig.id;
}
@@ -169,45 +189,69 @@ public class OndiloHandler extends BaseThingHandler {
this.recommendationId = 0; // Reset last processed recommendation ID
}
- public Instant updateLastMeasuresChannels(LastMeasure lastMeasures) {
- /*
- * The measures are received using the following units:
- * - Temperature: Celsius degrees (°C)
- * - ORP: millivolts (mV)
- * - Salt: milligrams per liter (mg/l)
- * - TDS: parts per million (ppm)
- * - Battery and RSSI: percent (%)
- */
- switch (lastMeasures.dataType) {
+ private void updateTrendChannel(String channel, String trendChannel, double value, Instant valueTime,
+ OndiloMeasureState lastMeasureState, Object unitOrType) {
+ Instant lastMeasureTime = lastMeasureState.time;
+ if (lastMeasureTime != null) {
+ if (!valueTime.equals(lastMeasureTime)) {
+ double delta = value - lastMeasureState.value;
+ if (unitOrType instanceof Unit> unit) {
+ updateState(trendChannel, new QuantityType<>(delta, (Unit>) unit));
+ } else { // DecimalType
+ updateState(trendChannel, new DecimalType(delta));
+ }
+ logger.trace(
+ "channel: {}, trendChannel: {}, value: {}, valueTime: {}, lastValue: {}, lastValueTime: {}, unitOrType: {} ==> delta: {}",
+ channel, trendChannel, value, valueTime.toString(), lastMeasureState.value,
+ lastMeasureTime.toString(), unitOrType.toString(), delta);
+ } // else: keep current value
+ } else {
+ updateState(trendChannel, UnDefType.UNDEF);
+ }
+ // Update the current value channel
+ if (unitOrType instanceof Unit> unit) {
+ updateState(channel, new QuantityType<>(value, (Unit>) unit));
+ } else { // DecimalType
+ updateState(channel, new DecimalType(value));
+ }
+
+ lastMeasureState.value = value;
+ lastMeasureState.time = valueTime;
+ }
+
+ public Instant updateLastMeasuresChannels(LastMeasure measure) {
+ Instant valueTime = parseUtcTimeToInstant(measure.valueTime);
+ switch (measure.dataType) {
case "temperature":
- updateState(CHANNEL_TEMPERATURE, new QuantityType<>(lastMeasures.value, SIUnits.CELSIUS));
+ updateTrendChannel(CHANNEL_TEMPERATURE, CHANNEL_TEMPERATURE_TREND, measure.value, valueTime,
+ lastTemperatureState, SIUnits.CELSIUS);
break;
case "ph":
- updateState(CHANNEL_PH, new DecimalType(lastMeasures.value));
+ updateTrendChannel(CHANNEL_PH, CHANNEL_PH_TREND, measure.value, valueTime, lastPhState,
+ DecimalType.class);
break;
case "orp":
- updateState(CHANNEL_ORP, new QuantityType<>(lastMeasures.value / 1000.0, Units.VOLT));
- // Convert mV to V
+ updateTrendChannel(CHANNEL_ORP, CHANNEL_ORP_TREND, measure.value / 1000.0, valueTime, lastOrpState,
+ Units.VOLT); // Convert mV to V
break;
case "salt":
- updateState(CHANNEL_SALT,
- new QuantityType<>(lastMeasures.value * 0.001, Units.KILOGRAM_PER_CUBICMETRE));
- // Convert mg/l to kg/m³
+ updateTrendChannel(CHANNEL_SALT, CHANNEL_SALT_TREND, measure.value * 0.001, valueTime, lastSaltState,
+ Units.KILOGRAM_PER_CUBICMETRE); // Convert mg/l to kg/m³
break;
case "tds":
- updateState(CHANNEL_TDS, new QuantityType<>(lastMeasures.value, Units.PARTS_PER_MILLION));
+ updateTrendChannel(CHANNEL_TDS, CHANNEL_TDS_TREND, measure.value, valueTime, lastTdsState,
+ Units.PARTS_PER_MILLION);
break;
case "battery":
- updateState(CHANNEL_BATTERY, new QuantityType<>(lastMeasures.value, Units.PERCENT));
+ updateState(CHANNEL_BATTERY, new QuantityType<>(measure.value, Units.PERCENT));
break;
case "rssi":
- updateState(CHANNEL_RSSI, new DecimalType(lastMeasures.value));
+ updateState(CHANNEL_RSSI, new DecimalType(measure.value));
break;
default:
- logger.warn("Unknown data type: {}", lastMeasures.dataType);
+ logger.warn("Unknown data type: {}", measure.dataType);
}
// Update value time channel (expect that it is the same for all measures)
- Instant valueTime = parseUtcTimeToInstant(lastMeasures.valueTime);
updateState(CHANNEL_VALUE_TIME, new DateTimeType(valueTime));
return valueTime;
}
@@ -223,6 +267,53 @@ public class OndiloHandler extends BaseThingHandler {
this.recommendationId = recommendation.id; // Update last processed recommendation ID
}
+ public void updatePool(Pool pool) {
+ Map properties = editProperties();
+ properties.put(PROPERTY_ONDILO_ID, String.valueOf(pool.id));
+ properties.put(PROPERTY_ONDILO_NAME, pool.name);
+ properties.put(PROPERTY_ONDILO_TYPE, pool.type);
+ properties.put(PROPERTY_ONDILO_VOLUME, pool.getVolume());
+ properties.put(PROPERTY_ONDILO_DISINFECTION, pool.getDisinfection());
+ properties.put(PROPERTY_ONDILO_ADDRESS, pool.getAddress());
+ properties.put(PROPERTY_ONDILO_LOCATION, pool.getLocation());
+ updateProperties(properties);
+ }
+
+ public void updatePoolInfo(PoolInfo poolInfo) {
+ Map properties = editProperties();
+ properties.put(PROPERTY_ONDILO_INFO_UUID, poolInfo.uuid);
+ properties.put(Thing.PROPERTY_SERIAL_NUMBER, poolInfo.serialNumber);
+ properties.put(Thing.PROPERTY_FIRMWARE_VERSION, poolInfo.swVersion);
+ updateProperties(properties);
+ }
+
+ public void updatePoolInfo(PoolConfiguration poolConfiguration) {
+ Map properties = editProperties();
+ properties.put(PROPERTY_ONDILO_INFO_POOL_GUY_NUMBER, poolConfiguration.poolGuyNumber);
+ properties.put(PROPERTY_ONDILO_INFO_MAINTENANCE_DAY,
+ poolConfiguration.getMaintenanceDay(localeProvider.getLocale()));
+ updateProperties(properties);
+ }
+
+ public void updatePoolConfiguration(PoolConfiguration poolConfiguration) {
+ updateState(CHANNEL_CONFIGURATION_TEMPERATURE_LOW,
+ new QuantityType<>(poolConfiguration.temperatureLow, SIUnits.CELSIUS));
+ updateState(CHANNEL_CONFIGURATION_TEMPERATURE_HIGH,
+ new QuantityType<>(poolConfiguration.temperatureHigh, SIUnits.CELSIUS));
+ updateState(CHANNEL_CONFIGURATION_PH_LOW, new DecimalType(poolConfiguration.phLow));
+ updateState(CHANNEL_CONFIGURATION_PH_HIGH, new DecimalType(poolConfiguration.phHigh));
+ updateState(CHANNEL_CONFIGURATION_ORP_LOW, new QuantityType<>(poolConfiguration.orpLow / 1000.0, Units.VOLT));
+ updateState(CHANNEL_CONFIGURATION_ORP_HIGH, new QuantityType<>(poolConfiguration.orpHigh / 1000.0, Units.VOLT));
+ updateState(CHANNEL_CONFIGURATION_SALT_LOW,
+ new QuantityType<>(poolConfiguration.saltLow * 0.001, Units.KILOGRAM_PER_CUBICMETRE));
+ updateState(CHANNEL_CONFIGURATION_SALT_HIGH,
+ new QuantityType<>(poolConfiguration.saltHigh * 0.001, Units.KILOGRAM_PER_CUBICMETRE));
+ updateState(CHANNEL_CONFIGURATION_TDS_LOW,
+ new QuantityType<>(poolConfiguration.tdsLow, Units.PARTS_PER_MILLION));
+ updateState(CHANNEL_CONFIGURATION_TDS_HIGH,
+ new QuantityType<>(poolConfiguration.tdsHigh, Units.PARTS_PER_MILLION));
+ }
+
@Nullable
private OndiloBridge getOndiloBridge() {
if (getBridge() instanceof Bridge bridge && bridge.getHandler() instanceof OndiloBridgeHandler bridgeHandler) {
diff --git a/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloHandlerFactory.java b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloHandlerFactory.java
index 54858f98e6..50e0b75b0d 100644
--- a/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloHandlerFactory.java
+++ b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloHandlerFactory.java
@@ -22,6 +22,7 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.ondilo.internal.discovery.OndiloDiscoveryService;
import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.config.discovery.DiscoveryService;
+import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
@@ -46,13 +47,15 @@ import org.slf4j.LoggerFactory;
public class OndiloHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(OndiloHandlerFactory.class);
private final OAuthFactory oAuthFactory;
+ private final LocaleProvider localeProvider;
private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BRIDGE, THING_TYPE_ONDILO);
private @Nullable ServiceRegistration> ondiloDiscoveryServiceRegistration;
private @Nullable OndiloDiscoveryService discoveryService;
@Activate
- public OndiloHandlerFactory(@Reference OAuthFactory oAuthFactory) {
+ public OndiloHandlerFactory(@Reference OAuthFactory oAuthFactory, @Reference LocaleProvider localeProvider) {
this.oAuthFactory = oAuthFactory;
+ this.localeProvider = localeProvider;
}
@Override
@@ -69,7 +72,7 @@ public class OndiloHandlerFactory extends BaseThingHandlerFactory {
registerOndiloDiscoveryService(handler);
return handler;
} else if (THING_TYPE_ONDILO.equals(thingTypeUID)) {
- return new OndiloHandler(thing);
+ return new OndiloHandler(thing, localeProvider);
}
return null;
}
diff --git a/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloMeasureState.java b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloMeasureState.java
new file mode 100644
index 0000000000..d530678497
--- /dev/null
+++ b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/OndiloMeasureState.java
@@ -0,0 +1,34 @@
+/*
+ * 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.ondilo.internal;
+
+import java.time.Instant;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * The {@link OndiloMeasureState} store last value and valueTime for trend calculation.
+ *
+ * @author MikeTheTux - Initial contribution
+ */
+@NonNullByDefault
+public class OndiloMeasureState {
+ public double value;
+ public @Nullable Instant time;
+
+ public OndiloMeasureState(double value, @Nullable Instant time) {
+ this.value = value;
+ this.time = time;
+ }
+}
diff --git a/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/discovery/OndiloDiscoveryService.java b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/discovery/OndiloDiscoveryService.java
index bf711ccd4a..69e85975d2 100644
--- a/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/discovery/OndiloDiscoveryService.java
+++ b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/discovery/OndiloDiscoveryService.java
@@ -96,16 +96,16 @@ public class OndiloDiscoveryService extends AbstractDiscoveryService {
ThingUID poolThingUid = new ThingUID(THING_TYPE_ONDILO, bridgeUID, String.valueOf(pool.id));
Map properties = new HashMap<>();
- properties.put(ONDILO_ID, pool.id);
- properties.put(ONDILO_NAME, pool.name);
- properties.put(ONDILO_TYPE, pool.type);
- properties.put(ONDILO_VOLUME, pool.getVolume());
- properties.put(ONDILO_DISINFECTION, pool.getDisinfection());
- properties.put(ONDILO_ADDRESS, pool.getAddress());
- properties.put(ONDILO_LOCATION, pool.getLocation());
+ properties.put(PROPERTY_ONDILO_ID, pool.id);
+ properties.put(PROPERTY_ONDILO_NAME, pool.name);
+ properties.put(PROPERTY_ONDILO_TYPE, pool.type);
+ properties.put(PROPERTY_ONDILO_VOLUME, pool.getVolume());
+ properties.put(PROPERTY_ONDILO_DISINFECTION, pool.getDisinfection());
+ properties.put(PROPERTY_ONDILO_ADDRESS, pool.getAddress());
+ properties.put(PROPERTY_ONDILO_LOCATION, pool.getLocation());
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(poolThingUid).withThingType(thingTypeUID)
- .withProperties(properties).withBridge(bridgeUID).withRepresentationProperty(ONDILO_ID)
+ .withProperties(properties).withBridge(bridgeUID).withRepresentationProperty(PROPERTY_ONDILO_ID)
.withLabel("Ondilo ICO: " + pool.name).build();
thingDiscovered(discoveryResult);
}
diff --git a/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/dto/PoolConfiguration.java b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/dto/PoolConfiguration.java
new file mode 100644
index 0000000000..3ce9304968
--- /dev/null
+++ b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/dto/PoolConfiguration.java
@@ -0,0 +1,84 @@
+/*
+ * 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.ondilo.internal.dto;
+
+import java.time.DayOfWeek;
+import java.time.format.TextStyle;
+import java.util.Locale;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link PoolConfiguration} DTO for representing Ondilo pool configurations.
+ *
+ * @author MikeTheTux - Initial contribution
+ */
+public class PoolConfiguration {
+ /*
+ * Example JSON representation:
+ * {
+ * "temperature_low": 10,
+ * "temperature_high": 30,
+ * "ph_low": 7.6,
+ * "ph_high": 8.5,
+ * "orp_low": 400,
+ * "orp_high": 900,
+ * "salt_low": 3000,
+ * "salt_high": 5000,
+ * "tds_low": 250,
+ * "tds_high": 2000,
+ * "pool_guy_number": "0123456789",
+ * "maintenance_day": 2
+ * }
+ */
+ @SerializedName("temperature_low")
+ public int temperatureLow;
+
+ @SerializedName("temperature_high")
+ public int temperatureHigh;
+
+ @SerializedName("ph_low")
+ public double phLow;
+
+ @SerializedName("ph_high")
+ public double phHigh;
+
+ @SerializedName("orp_low")
+ public int orpLow;
+
+ @SerializedName("orp_high")
+ public int orpHigh;
+
+ @SerializedName("salt_low")
+ public int saltLow;
+
+ @SerializedName("salt_high")
+ public int saltHigh;
+
+ @SerializedName("tds_low")
+ public int tdsLow;
+
+ @SerializedName("tds_high")
+ public int tdsHigh;
+
+ @SerializedName("pool_guy_number")
+ public String poolGuyNumber;
+
+ @SerializedName("maintenance_day")
+ public int maintenanceDay;
+
+ public String getMaintenanceDay(Locale locale) {
+ DayOfWeek dayOfWeek = DayOfWeek.of((maintenanceDay % 7) + 1); // Change from week start Sunday to Monday
+ return dayOfWeek.getDisplayName(TextStyle.FULL, locale);
+ }
+}
diff --git a/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/dto/PoolInfo.java b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/dto/PoolInfo.java
new file mode 100644
index 0000000000..e447abd3ab
--- /dev/null
+++ b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/dto/PoolInfo.java
@@ -0,0 +1,36 @@
+/*
+ * 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.ondilo.internal.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link PoolInfo} DTO for representing Ondilo pool infos.
+ *
+ * @author MikeTheTux - Initial contribution
+ */
+public class PoolInfo {
+ /*
+ * Example JSON representation:
+ * {
+ * "uuid": "1234567890ABCDEF",
+ * "serial_number": "SN00001",
+ * "sw_version": "1.5.1-stable"
+ * }
+ */
+ public String uuid;
+ @SerializedName("serial_number")
+ public String serialNumber;
+ @SerializedName("sw_version")
+ public String swVersion;
+}
diff --git a/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/dto/UserInfo.java b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/dto/UserInfo.java
new file mode 100644
index 0000000000..f3d777ef34
--- /dev/null
+++ b/bundles/org.openhab.binding.ondilo/src/main/java/org/openhab/binding/ondilo/internal/dto/UserInfo.java
@@ -0,0 +1,40 @@
+/*
+ * 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.ondilo.internal.dto;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link UserInfo} DTO for representing Ondilo user infos.
+ *
+ * @author MikeTheTux - Initial contribution
+ */
+public class UserInfo {
+ /*
+ * Example JSON representation:
+ * {
+ * "lastname": "Doe",
+ * "firstname": "John",
+ * "email": "john@doe.org"
+ * }
+ */
+ @SerializedName("lastname")
+ public String lastName;
+ @SerializedName("firstname")
+ public String firstName;
+ public String email;
+
+ public String getUserInfo() {
+ return lastName + " " + firstName + " (" + email + ")";
+ }
+}
diff --git a/bundles/org.openhab.binding.ondilo/src/main/resources/OH-INF/i18n/ondilo.properties b/bundles/org.openhab.binding.ondilo/src/main/resources/OH-INF/i18n/ondilo.properties
index 5b9d203970..3529f278a6 100644
--- a/bundles/org.openhab.binding.ondilo/src/main/resources/OH-INF/i18n/ondilo.properties
+++ b/bundles/org.openhab.binding.ondilo/src/main/resources/OH-INF/i18n/ondilo.properties
@@ -21,6 +21,17 @@ thing-type.config.ondilo.ondilo.id.description = The Id of an Ondilo ICO Pool/Sp
# channel group types
+channel-group-type.ondilo.configuration.label = Configurations
+channel-group-type.ondilo.configuration.channel.orp-high.label = Maximum ORP
+channel-group-type.ondilo.configuration.channel.orp-high.description = Maximum Oxidation-Reduction Potential (ORP)
+channel-group-type.ondilo.configuration.channel.ph-high.label = Maximum PH Level
+channel-group-type.ondilo.configuration.channel.ph-high.description = Maximum PH level
+channel-group-type.ondilo.configuration.channel.salt-high.label = Maximum Salt Level
+channel-group-type.ondilo.configuration.channel.salt-high.description = Maximum salt level
+channel-group-type.ondilo.configuration.channel.tds-high.label = Maximum TDS
+channel-group-type.ondilo.configuration.channel.tds-high.description = Maximum Total Dissolved Solids (TDS)
+channel-group-type.ondilo.configuration.channel.temperature-high.label = Maximum Water Temperature
+channel-group-type.ondilo.configuration.channel.temperature-high.description = Maximum water temperature
channel-group-type.ondilo.measure.label = Measures
channel-group-type.ondilo.recommendation.label = Recommendations
@@ -34,22 +45,42 @@ channel-type.ondilo.id.label = ID
channel-type.ondilo.id.description = ID of the current recommendation
channel-type.ondilo.message.label = Message
channel-type.ondilo.message.description = Message of the current recommendation
+channel-type.ondilo.orp-low-high.label = Minimum ORP
+channel-type.ondilo.orp-low-high.description = Minimum Oxidation-Reduction Potential (ORP)
+channel-type.ondilo.orp-trend.label = ORP Delta
+channel-type.ondilo.orp-trend.description = Change in ORP since last measure
channel-type.ondilo.orp.label = ORP
channel-type.ondilo.orp.description = Oxidation-Reduction Potential (ORP)
+channel-type.ondilo.ph-low-high.label = Minimum PH Level
+channel-type.ondilo.ph-low-high.description = Minimum PH level
+channel-type.ondilo.ph-trend.label = PH Level Delta
+channel-type.ondilo.ph-trend.description = Change in PH level since last measure
channel-type.ondilo.ph.label = PH Level
channel-type.ondilo.ph.description = PH level
channel-type.ondilo.poll-update.label = Poll Update
channel-type.ondilo.poll-update.description = Poll status update from the cloud (get latest measures, not a trigger for new measures)
channel-type.ondilo.rssi.label = RSSI
channel-type.ondilo.rssi.description = Signal strength (RSSI)
+channel-type.ondilo.salt-low-high.label = Minimum Salt Level
+channel-type.ondilo.salt-low-high.description = Minimum salt level
+channel-type.ondilo.salt-trend.label = Salt Delta
+channel-type.ondilo.salt-trend.description = Change in salt level since last measure
channel-type.ondilo.salt.label = Salt
channel-type.ondilo.salt.description = Salt level
channel-type.ondilo.status.label = Status
channel-type.ondilo.status.description = Status of the current recommendation
channel-type.ondilo.status.state.option.waiting = waiting
channel-type.ondilo.status.state.option.ok = ok
+channel-type.ondilo.tds-low-high.label = Minimum TDS
+channel-type.ondilo.tds-low-high.description = Minimum Total Dissolved Solids (TDS)
+channel-type.ondilo.tds-trend.label = TDS Delta
+channel-type.ondilo.tds-trend.description = Change in TDS since last measure
channel-type.ondilo.tds.label = TDS
channel-type.ondilo.tds.description = Total dissolved solids
+channel-type.ondilo.temperature-low-high.label = Minimum Water Temperature
+channel-type.ondilo.temperature-low-high.description = Minimum water temperature
+channel-type.ondilo.temperature-trend.label = Water Temperature Delta
+channel-type.ondilo.temperature-trend.description = Change in water temperature since last measure
channel-type.ondilo.temperature.label = Water Temperature
channel-type.ondilo.temperature.description = Water temperature
channel-type.ondilo.title.label = Title
diff --git a/bundles/org.openhab.binding.ondilo/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.ondilo/src/main/resources/OH-INF/thing/thing-types.xml
index 93046744b2..fc7cbe196e 100644
--- a/bundles/org.openhab.binding.ondilo/src/main/resources/OH-INF/thing/thing-types.xml
+++ b/bundles/org.openhab.binding.ondilo/src/main/resources/OH-INF/thing/thing-types.xml
@@ -14,6 +14,10 @@
+
+
+
+
@@ -42,6 +46,7 @@
+
@@ -51,6 +56,10 @@
+
+
+
+ 1
@@ -65,10 +74,15 @@
+
+
+
+
+
@@ -88,13 +102,44 @@
+
+
+
+
+
+
+ Maximum water temperature
+
+
+
+
+ Maximum PH level
+
+
+
+
+ Maximum Oxidation-Reduction Potential (ORP)
+
+
+
+
+ Maximum salt level
+
+
+
+
+ Maximum Total Dissolved Solids (TDS)
+
+
+
+
Switch
Poll status update from the cloud (get latest measures, not a trigger for new measures)
- Control
+ Setpoint
@@ -109,6 +154,16 @@
+
+ Number:Temperature
+
+ Change in water temperature since last measure
+
+ Measurement
+ Temperature
+
+
+
Number
@@ -116,7 +171,16 @@
Measurement
-
+
+
+
+ Number
+
+ Change in PH level since last measure
+
+ Measurement
+
+
Number:ElectricPotential
@@ -127,6 +191,15 @@
+
+ Number:ElectricPotential
+
+ Change in ORP since last measure
+
+ Measurement
+
+
+
Number:Density
@@ -136,6 +209,15 @@
+
+ Number:Density
+
+ Change in salt level since last measure
+
+ Measurement
+
+
+
Number:Dimensionless
@@ -145,6 +227,15 @@
+
+ Number:Dimensionless
+
+ Change in TDS since last measure
+
+ Measurement
+
+
+
+
+
+
+ Number:Temperature
+
+ Minimum water temperature
+
+ Setpoint
+ Temperature
+
+
+
+
+ Number
+
+ Minimum PH level
+
+ Setpoint
+
+
+
+
+ Number:ElectricPotential
+
+ Minimum Oxidation-Reduction Potential (ORP)
+
+ Setpoint
+
+
+
+
+ Number:Density
+
+ Minimum salt level
+
+ Setpoint
+
+
+
+
+ Number:Dimensionless
+
+ Minimum Total Dissolved Solids (TDS)
+
+ Setpoint
+
+
+
diff --git a/bundles/org.openhab.binding.ondilo/src/main/resources/OH-INF/update/update.xml b/bundles/org.openhab.binding.ondilo/src/main/resources/OH-INF/update/update.xml
new file mode 100644
index 0000000000..5bf3f68465
--- /dev/null
+++ b/bundles/org.openhab.binding.ondilo/src/main/resources/OH-INF/update/update.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+ ondilo:temperature-trend
+
+
+ ondilo:ph-trend
+
+
+ ondilo:orp-trend
+
+
+ ondilo:salt-trend
+
+
+ ondilo:tds-trend
+
+
+
+ ondilo:temperature-low-high
+
+
+ ondilo:temperature-low-high
+
+ Maximum water temperature
+
+
+ ondilo:ph-low-high
+
+
+ ondilo:ph-low-high
+
+ Maximum PH level
+
+
+ ondilo:orp-low-high
+
+
+ ondilo:orp-low-high
+
+ Maximum Oxidation-Reduction Potential (ORP)
+
+
+ ondilo:salt-low-high
+
+
+ ondilo:salt-low-high
+
+ Maximum salt level
+
+
+ ondilo:tds-low-high
+
+
+ ondilo:tds-low-high
+
+ Maximum Total Dissolved Solids (TDS)
+
+
+
+