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 @@ -240,4 +331,52 @@ + + + + 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) + + + +