[ondilo] Add new Channels and Properties (#18978)

* increased buffer to 3min
fixed error with assignment to local instead of global variable

Signed-off-by: Michael Weger <weger.michael@gmx.net>
This commit is contained in:
MikeTheTux
2025-07-22 00:42:06 +02:00
committed by GitHub
parent e8b5839c5d
commit 445c17891b
15 changed files with 708 additions and 134 deletions
+39 -16
View File
@@ -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`)<br/>`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_received_from_discovery>" [ id="<id_received_from_discovery>" ] {
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)
@@ -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> T request(String requestMethod, String endpoint, Class<T> 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);
@@ -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";
@@ -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<Pool> 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<Pool> pools = gson.fromJson(poolsJson, new TypeToken<List<Pool>>() {
}.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<LastMeasure> lastMeasures = gson.fromJson(poolsJson, new TypeToken<List<LastMeasure>>() {
}.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<Recommendation> recommendations = gson.fromJson(recommendationsJson,
new TypeToken<List<Recommendation>>() {
}.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);
@@ -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<String, String> properties = editProperties();
properties.put(PROPERTY_BRIDGE_USER_INFO, userInfo.getUserInfo());
updateProperties(properties);
}
@Override
public void dispose() {
String clientSecret = thing.getUID().getAsString();
@@ -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<String> 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<String, String> 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<String, String> 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<String, String> 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) {
@@ -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<ThingTypeUID> 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;
}
@@ -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;
}
}
@@ -96,16 +96,16 @@ public class OndiloDiscoveryService extends AbstractDiscoveryService {
ThingUID poolThingUid = new ThingUID(THING_TYPE_ONDILO, bridgeUID, String.valueOf(pool.id));
Map<String, Object> 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);
}
@@ -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);
}
}
@@ -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;
}
@@ -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 + ")";
}
}
@@ -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
@@ -14,6 +14,10 @@
<channel id="poll-update" typeId="poll-update"/>
</channels>
<properties>
<property name="userInfo"/>
</properties>
<config-description>
<parameter name="url" type="text" required="true">
<label>openHAB URL</label>
@@ -42,6 +46,7 @@
<channel-groups>
<channel-group id="measure" typeId="measure"/>
<channel-group id="recommendation" typeId="recommendation"/>
<channel-group id="configuration" typeId="configuration"/>
</channel-groups>
<properties>
@@ -51,6 +56,10 @@
<property name="disinfection"/>
<property name="address"/>
<property name="location"/>
<property name="uuid"/>
<property name="poolGuyNumber"/>
<property name="maintenanceDay"/>
<property name="thingTypeVersion">1</property>
</properties>
<config-description>
@@ -65,10 +74,15 @@
<label>Measures</label>
<channels>
<channel id="temperature" typeId="temperature"/>
<channel id="temperature-trend" typeId="temperature-trend"/>
<channel id="ph" typeId="ph"/>
<channel id="ph-trend" typeId="ph-trend"/>
<channel id="orp" typeId="orp"/>
<channel id="orp-trend" typeId="orp-trend"/>
<channel id="salt" typeId="salt"/>
<channel id="salt-trend" typeId="salt-trend"/>
<channel id="tds" typeId="tds"/>
<channel id="tds-trend" typeId="tds-trend"/>
<channel id="battery" typeId="system.battery-level"/>
<channel id="rssi" typeId="rssi"/>
<channel id="value-time" typeId="value-time"/>
@@ -88,13 +102,44 @@
</channels>
</channel-group-type>
<channel-group-type id="configuration">
<label>Configurations</label>
<channels>
<channel id="temperature-low" typeId="temperature-low-high"/>
<channel id="temperature-high" typeId="temperature-low-high">
<label>Maximum Water Temperature</label>
<description>Maximum water temperature</description>
</channel>
<channel id="ph-low" typeId="ph-low-high"/>
<channel id="ph-high" typeId="ph-low-high">
<label>Maximum PH Level</label>
<description>Maximum PH level</description>
</channel>
<channel id="orp-low" typeId="orp-low-high"/>
<channel id="orp-high" typeId="orp-low-high">
<label>Maximum ORP</label>
<description>Maximum Oxidation-Reduction Potential (ORP)</description>
</channel>
<channel id="salt-low" typeId="salt-low-high"/>
<channel id="salt-high" typeId="salt-low-high">
<label>Maximum Salt Level</label>
<description>Maximum salt level</description>
</channel>
<channel id="tds-low" typeId="tds-low-high"/>
<channel id="tds-high" typeId="tds-low-high">
<label>Maximum TDS</label>
<description>Maximum Total Dissolved Solids (TDS)</description>
</channel>
</channels>
</channel-group-type>
<!-- Bridge Channel Types -->
<channel-type id="poll-update" advanced="true">
<item-type>Switch</item-type>
<label>Poll Update</label>
<description>Poll status update from the cloud (get latest measures, not a trigger for new measures)</description>
<tags>
<tag>Control</tag>
<tag>Setpoint</tag>
</tags>
</channel-type>
@@ -109,6 +154,16 @@
</tags>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="temperature-trend" advanced="true">
<item-type unitHint="°C">Number:Temperature</item-type>
<label>Water Temperature Delta</label>
<description>Change in water temperature since last measure</description>
<tags>
<tag>Measurement</tag>
<tag>Temperature</tag>
</tags>
<state readOnly="true" pattern="Δ %+.1f %unit%"/>
</channel-type>
<channel-type id="ph">
<item-type>Number</item-type>
<label>PH Level</label>
@@ -116,7 +171,16 @@
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true" pattern="%d"/>
<state readOnly="true" pattern="%.2f"/>
</channel-type>
<channel-type id="ph-trend" advanced="true">
<item-type>Number</item-type>
<label>PH Level Delta</label>
<description>Change in PH level since last measure</description>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true" pattern="Δ %+.2f"/>
</channel-type>
<channel-type id="orp">
<item-type unitHint="mV">Number:ElectricPotential</item-type>
@@ -127,6 +191,15 @@
</tags>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="orp-trend" advanced="true">
<item-type unitHint="mV">Number:ElectricPotential</item-type>
<label>ORP Delta</label>
<description>Change in ORP since last measure</description>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true" pattern="Δ %+.1f %unit%"/>
</channel-type>
<channel-type id="salt">
<item-type unitHint="mg/l">Number:Density</item-type>
<label>Salt</label>
@@ -136,6 +209,15 @@
</tags>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="salt-trend" advanced="true">
<item-type unitHint="mg/l">Number:Density</item-type>
<label>Salt Delta</label>
<description>Change in salt level since last measure</description>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true" pattern="Δ %+.0f %unit%"/>
</channel-type>
<channel-type id="tds">
<item-type unitHint="ppm">Number:Dimensionless</item-type>
<label>TDS</label>
@@ -145,6 +227,15 @@
</tags>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="tds-trend" advanced="true">
<item-type unitHint="ppm">Number:Dimensionless</item-type>
<label>TDS Delta</label>
<description>Change in TDS since last measure</description>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true" pattern="Δ %+.1f %unit%"/>
</channel-type>
<channel-type id="rssi">
<item-type>Number</item-type>
<label>RSSI</label>
@@ -240,4 +331,52 @@
</tags>
<state readOnly="true"/>
</channel-type>
<!-- Configuration Channel Types -->
<channel-type id="temperature-low-high" advanced="true">
<item-type unitHint="°C">Number:Temperature</item-type>
<label>Minimum Water Temperature</label>
<description>Minimum water temperature</description>
<tags>
<tag>Setpoint</tag>
<tag>Temperature</tag>
</tags>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="ph-low-high" advanced="true">
<item-type>Number</item-type>
<label>Minimum PH Level</label>
<description>Minimum PH level</description>
<tags>
<tag>Setpoint</tag>
</tags>
<state readOnly="true" pattern="%.2f"/>
</channel-type>
<channel-type id="orp-low-high" advanced="true">
<item-type unitHint="mV">Number:ElectricPotential</item-type>
<label>Minimum ORP</label>
<description>Minimum Oxidation-Reduction Potential (ORP)</description>
<tags>
<tag>Setpoint</tag>
</tags>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="salt-low-high" advanced="true">
<item-type unitHint="mg/l">Number:Density</item-type>
<label>Minimum Salt Level</label>
<description>Minimum salt level</description>
<tags>
<tag>Setpoint</tag>
</tags>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="tds-low-high" advanced="true">
<item-type unitHint="ppm">Number:Dimensionless</item-type>
<label>Minimum TDS</label>
<description>Minimum Total Dissolved Solids (TDS)</description>
<tags>
<tag>Setpoint</tag>
</tags>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
</thing:thing-descriptions>
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
<thing-type uid="ondilo:ondilo">
<instruction-set targetVersion="1">
<add-channel id="temperature-trend" groupIds="measure">
<type>ondilo:temperature-trend</type>
</add-channel>
<add-channel id="ph-trend" groupIds="measure">
<type>ondilo:ph-trend</type>
</add-channel>
<add-channel id="orp-trend" groupIds="measure">
<type>ondilo:orp-trend</type>
</add-channel>
<add-channel id="salt-trend" groupIds="measure">
<type>ondilo:salt-trend</type>
</add-channel>
<add-channel id="tds-trend" groupIds="measure">
<type>ondilo:tds-trend</type>
</add-channel>
<add-channel id="temperature-low" groupIds="configuration">
<type>ondilo:temperature-low-high</type>
</add-channel>
<add-channel id="temperature-high" groupIds="configuration">
<type>ondilo:temperature-low-high</type>
<label>Maximum Water Temperature</label>
<description>Maximum water temperature</description>
</add-channel>
<add-channel id="ph-low" groupIds="configuration">
<type>ondilo:ph-low-high</type>
</add-channel>
<add-channel id="ph-high" groupIds="configuration">
<type>ondilo:ph-low-high</type>
<label>Maximum PH Level</label>
<description>Maximum PH level</description>
</add-channel>
<add-channel id="orp-low" groupIds="configuration">
<type>ondilo:orp-low-high</type>
</add-channel>
<add-channel id="orp-high" groupIds="configuration">
<type>ondilo:orp-low-high</type>
<label>Maximum ORP</label>
<description>Maximum Oxidation-Reduction Potential (ORP)</description>
</add-channel>
<add-channel id="salt-low" groupIds="configuration">
<type>ondilo:salt-low-high</type>
</add-channel>
<add-channel id="salt-high" groupIds="configuration">
<type>ondilo:salt-low-high</type>
<label>Maximum Salt Level</label>
<description>Maximum salt level</description>
</add-channel>
<add-channel id="tds-low" groupIds="configuration">
<type>ondilo:tds-low-high</type>
</add-channel>
<add-channel id="tds-high" groupIds="configuration">
<type>ondilo:tds-low-high</type>
<label>Maximum TDS</label>
<description>Maximum Total Dissolved Solids (TDS)</description>
</add-channel>
</instruction-set>
</thing-type>
</update:update-descriptions>