-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
[ondilo] Initial contribution #18914
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 18 commits
86456fa
94088e6
84077eb
aa86621
f5ab2c7
b167468
4faa764
c06e7e7
d5074ed
11bc68f
6b1d062
e653b9d
aa4acba
2edeab6
53a8644
5ae9efb
547fd11
d45a645
5f3e63f
4dc7325
ccaad7b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| This content is produced and maintained by the openHAB project. | ||
|
|
||
| * Project home: https://www.openhab.org | ||
|
|
||
| == Declared Project Licenses | ||
|
|
||
| This program and the accompanying materials are made available under the terms | ||
| of the Eclipse Public License 2.0 which is available at | ||
| https://www.eclipse.org/legal/epl-2.0/. | ||
|
|
||
| == Source Code | ||
|
|
||
| https://github.com/openhab/openhab-addons |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| # Ondilo Binding | ||
|
|
||
| This binding integrates Ondilo ICO pool monitoring devices with openHAB, allowing you to monitor and automate your pool environment using openHAB’s rules and UI. | ||
|
|
||
| ## Supported Things | ||
|
|
||
| `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. | ||
|
|
||
| ## Discovery | ||
|
|
||
| Ondilo ICOs are discovered automatically after the `account` is authorized and online. | ||
| Each Ondilo ICO will appear as a new Thing in the inbox. | ||
|
|
||
| ## Thing Configuration | ||
|
|
||
| ### `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`). | ||
|
|
||
| ### `ondilo` Thing Configuration | ||
|
|
||
| - **id**: The Id of an Ondilo ICO device. Set via discovery service (e.g. `12345`) | ||
|
|
||
| Ondilo ICO takes measures every hour. | ||
| Higher polling will not increase the update interval. | ||
| The binding automatically adjusts the polling schedule to match the expected time of the next measure, which is typically 1 hour (plus 1.5 minutes buffer) after the previous measure. | ||
|
|
||
| The requests to the Ondilo Customer API are limited to the following per user quotas: | ||
|
|
||
| - 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. | ||
|
|
||
| ## Channels | ||
|
|
||
| ### `account` Channels | ||
|
|
||
| | Channel ID | Type | Advanced | Access | Description | | ||
| |---------------------------|-------------------------|----------|--------|--------------------------------------------------------| | ||
| | poll-update | Switch | true | R/W | Poll status update from the cloud (get latest measures, not a trigger for new measures) | | ||
|
|
||
| ### 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 | | ||
|
|
||
| ### Recommendations Channels | ||
|
|
||
| | Channel ID | Type | Advanced | Access | Description | | ||
| |---------------------------|-------------------------|----------|--------|--------------------------------------------------------| | ||
| | recommendation-id | Number | true | R | Unique ID of the current recommendation | | ||
| | recommendation-title | String | false | R | Title of the current recommendation | | ||
| | recommendation-message | String | false | R | Message of the current recommendation | | ||
| | recommendation-created-at | String | true | R | Creation time of the current recommendation | | ||
| | recommendation-updated-at | String | true | R | Last update time of the current recommendation | | ||
| | 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 | | ||
|
|
||
| ## 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>" ] { | ||
| } | ||
| ``` | ||
|
|
||
| ### Item Configuration | ||
|
|
||
| ```java | ||
| Number:Temperature Ondilo_Temperature "Pool Temperature [%.1f %unit%]" { channel="ondilo:ondilo:ondiloAccount:12345:measure#temperature" } | ||
| Number Ondilo_pH "Pool pH [%d]" { channel="ondilo:ondilo:ondiloAccount:12345:measure#ph" } | ||
| Number:ElectricPotential Ondilo_ORP "Pool ORP [%.1f %unit%]" { channel="ondilo:ondilo:ondiloAccount:12345:measure#orp" } | ||
| Number:Density Ondilo_Salt "Pool Salt [%.0f %unit%]" { channel="ondilo:ondilo:ondiloAccount:12345:measure#salt" } | ||
| Number:Dimensionless Ondilo_Battery "Pool Battery [%d %]" { channel="ondilo:ondilo:ondiloAccount:12345:measure#battery" } | ||
| Number:Dimensionless Ondilo_RSSI "Pool RSSI [%.0f]" { channel="ondilo:ondilo:ondiloAccount:12345:measure#rssi" } | ||
|
|
||
| String Ondilo_RecTitle "Recommendation Title [%s]" { channel="ondilo:ondilo:ondiloAccount:12345:recommendation#title" } | ||
| String Ondilo_RecMessage "Recommendation Message [%s]" { channel="ondilo:ondilo:ondiloAccount:12345:recommendation#message" } | ||
| String Ondilo_RecStatus "Recommendation Status [%s]" { channel="ondilo:ondilo:ondiloAccount:12345:recommendation#status" } | ||
| ``` | ||
|
|
||
| ### Sitemap Configuration | ||
|
|
||
| ```perl | ||
| sitemap demo label="Ondilo ICO" { | ||
| Frame label="Measures" { | ||
| Text item=Ondilo_Temperature | ||
| Text item=Ondilo_pH | ||
| Text item=Ondilo_ORP | ||
| Text item=Ondilo_Battery | ||
| Text item=Ondilo_RSSI | ||
| } | ||
| Frame label="Recommendations" { | ||
| Text item=Ondilo_RecTitle | ||
| Text item=Ondilo_RecMessage | ||
| Text item=Ondilo_RecStatus | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| - If authorization fails, check the openHAB log for error messages and verify your redirect URI `url` | ||
| - For more details, enable TRACE logging for `org.openhab.binding.ondilo` | ||
|
|
||
| ## Resources | ||
|
|
||
| - [Ondilo API Documentation](https://interop.ondilo.com/docs/api/customer/v1) | ||
| - [openHAB Community Forum](https://community.openhab.org/t/request-ondilo-binding/98164) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
|
|
||
| <modelVersion>4.0.0</modelVersion> | ||
|
|
||
| <parent> | ||
| <groupId>org.openhab.addons.bundles</groupId> | ||
| <artifactId>org.openhab.addons.reactor.bundles</artifactId> | ||
| <version>5.0.0-SNAPSHOT</version> | ||
| </parent> | ||
|
|
||
| <artifactId>org.openhab.binding.ondilo</artifactId> | ||
|
|
||
| <name>openHAB Add-ons :: Bundles :: Ondilo Binding</name> | ||
|
|
||
| </project> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <features name="org.openhab.binding.ondilo-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0"> | ||
| <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository> | ||
|
|
||
| <feature name="openhab-binding-ondilo" description="Ondilo Binding" version="${project.version}"> | ||
| <feature>openhab-runtime-base</feature> | ||
| <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.ondilo/${project.version}</bundle> | ||
| </feature> | ||
| </features> |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,131 @@ | ||||||
| /* | ||||||
| * 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.io.IOException; | ||||||
| import java.io.InputStream; | ||||||
| import java.io.InterruptedIOException; | ||||||
| import java.net.HttpURLConnection; | ||||||
| import java.net.URI; | ||||||
| import java.net.URISyntaxException; | ||||||
| import java.net.URL; | ||||||
| import java.time.Instant; | ||||||
| import java.util.Scanner; | ||||||
|
|
||||||
| import org.eclipse.jdt.annotation.NonNullByDefault; | ||||||
| import org.eclipse.jdt.annotation.Nullable; | ||||||
| import org.openhab.core.auth.client.oauth2.AccessTokenResponse; | ||||||
| import org.openhab.core.auth.client.oauth2.OAuthClientService; | ||||||
| import org.openhab.core.auth.client.oauth2.OAuthException; | ||||||
| import org.openhab.core.auth.client.oauth2.OAuthResponseException; | ||||||
| import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | ||||||
|
|
||||||
| /** | ||||||
| * The {@link OndiloApiClient} for accessing the Ondilo API using OAuth2 authentication. | ||||||
| * handlers. | ||||||
| * | ||||||
| * @author MikeTheTux - Initial contribution | ||||||
| */ | ||||||
| @NonNullByDefault | ||||||
| public class OndiloApiClient { | ||||||
| private final Logger logger = LoggerFactory.getLogger(OndiloApiClient.class); | ||||||
| private @Nullable OAuthClientService oAuthService; | ||||||
| private @Nullable String bearer; | ||||||
| private @Nullable AccessTokenResponse accessTokenResponse; | ||||||
| private static final String ONDILO_API_URL = "https://interop.ondilo.com/api/customer/v1"; | ||||||
|
|
||||||
| public OndiloApiClient(OAuthClientService oAuthService, AccessTokenResponse accessTokenResponse) { | ||||||
| this.oAuthService = oAuthService; | ||||||
| this.accessTokenResponse = accessTokenResponse; | ||||||
| this.bearer = accessTokenResponse.getAccessToken(); | ||||||
| logger.trace("OndiloApiClient initialized with OAuth2 service and bearer token"); | ||||||
| } | ||||||
|
|
||||||
| @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); | ||||||
| } | ||||||
| } 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.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); | ||||||
| } | ||||||
| } 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; | ||||||
| } | ||||||
|
|
||||||
| private void refreshAccessTokenIfNeeded() { | ||||||
| OAuthClientService oAuthService = this.oAuthService; | ||||||
| AccessTokenResponse accessTokenResponse = this.accessTokenResponse; | ||||||
| if (oAuthService != null && accessTokenResponse != null) { | ||||||
| if (accessTokenResponse.isExpired(Instant.now(), 120)) { | ||||||
| try { | ||||||
| this.accessTokenResponse = oAuthService.refreshToken(); | ||||||
| this.bearer = accessTokenResponse.getAccessToken(); | ||||||
|
||||||
| this.bearer = accessTokenResponse.getAccessToken(); | |
| this.bearer = this.accessTokenResponse.getAccessToken(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.accessTokenResponse and accessTokenResponse are both pointing to the same global object:
AccessTokenResponse accessTokenResponse = this.accessTokenResponse;
accessTokenResponse is null-safe, compared to this.accessTokenResponse:
if (oAuthService != null && accessTokenResponse != null) {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the global var is re-assigned, the local var is still referencing the old object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you are right - the assignment makes the difference
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost, the mentioned warning is is introduced as expected. It is a race condition that should be checked.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| /* | ||
| * 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 org.eclipse.jdt.annotation.NonNullByDefault; | ||
| import org.openhab.core.thing.ThingTypeUID; | ||
|
|
||
| /** | ||
| * The {@link OndiloBindingConstants} class defines common constants, which are | ||
| * used across the whole binding. | ||
| * | ||
| * @author MikeTheTux - Initial contribution | ||
| */ | ||
| @NonNullByDefault | ||
| public class OndiloBindingConstants { | ||
|
|
||
| private static final String BINDING_ID = "ondilo"; | ||
|
|
||
| // List of all Thing Type UIDs | ||
| 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 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"; | ||
|
|
||
| // 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_PH = GROUP_MEASURES + "ph"; | ||
| public static final String CHANNEL_ORP = GROUP_MEASURES + "orp"; | ||
| public static final String CHANNEL_SALT = GROUP_MEASURES + "salt"; | ||
| public static final String CHANNEL_TDS = GROUP_MEASURES + "tds"; | ||
| 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"; | ||
|
|
||
| // Ondilo Thing Recommendations Channel ids | ||
| public static final String GROUP_RECOMMENDATIONS = "recommendation#"; | ||
|
|
||
| public static final String CHANNEL_RECOMMENDATION_ID = GROUP_RECOMMENDATIONS + "id"; | ||
| public static final String CHANNEL_RECOMMENDATION_TITLE = GROUP_RECOMMENDATIONS + "title"; | ||
| public static final String CHANNEL_RECOMMENDATION_MESSAGE = GROUP_RECOMMENDATIONS + "message"; | ||
| public static final String CHANNEL_RECOMMENDATION_CREATED_AT = GROUP_RECOMMENDATIONS + "created-at"; | ||
| public static final String CHANNEL_RECOMMENDATION_UPDATED_AT = GROUP_RECOMMENDATIONS + "updated-at"; | ||
| public static final String CHANNEL_RECOMMENDATION_STATUS = GROUP_RECOMMENDATIONS + "status"; | ||
| public static final String CHANNEL_RECOMMENDATION_DEADLINE = GROUP_RECOMMENDATIONS + "deadline"; | ||
|
|
||
| // 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"; | ||
| public static final String I18N_OAUTH2_ERROR = "@text/thing.ondilo.bridge.config.oauth2.error"; | ||
| public static final String I18N_OAUTH2_INTERRUPTED = "@text/thing.ondilo.bridge.config.oauth2.interrupted"; | ||
| public static final String I18N_ID_INVALID = "@text/thing.ondilo.ondilo.config.id.invalid"; | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.